@foss.global/gitmanager 2.0.0 → 2.2.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.
Files changed (41) hide show
  1. package/.smartconfig.json +3 -1
  2. package/changelog.md +30 -0
  3. package/dist_rust/rustgitmanager_linux_amd64 +0 -0
  4. package/dist_rust/rustgitmanager_linux_amd64.tsrust-build.json +5 -5
  5. package/dist_rust/rustgitmanager_linux_arm64 +0 -0
  6. package/dist_rust/rustgitmanager_linux_arm64.tsrust-build.json +5 -5
  7. package/dist_ts/00_commitinfo_data.js +1 -1
  8. package/dist_ts/classes.archiveinspection.d.ts +11 -0
  9. package/dist_ts/classes.archiveinspection.js +205 -0
  10. package/dist_ts/classes.gitmanager.d.ts +19 -1
  11. package/dist_ts/classes.gitmanager.js +54 -1
  12. package/dist_ts/classes.gitmanagererror.d.ts +11 -0
  13. package/dist_ts/classes.gitmanagererror.js +22 -0
  14. package/dist_ts/index.d.ts +2 -0
  15. package/dist_ts/index.js +3 -1
  16. package/dist_ts/models.d.ts +54 -0
  17. package/dist_ts/rust-gitmanager-bridge.d.ts +5 -1
  18. package/dist_ts/rust-gitmanager-bridge.js +41 -2
  19. package/native-notices/crates/cc-1.8.0/license-apache +201 -0
  20. package/native-notices/crates/cc-1.8.0/license-mit +25 -0
  21. package/native-notices/crates/find-msvc-tools-0.1.14/license-apache +201 -0
  22. package/native-notices/crates/find-msvc-tools-0.1.14/license-mit +25 -0
  23. package/native-notices/crates/shlex-2.0.1/license-apache +13 -0
  24. package/native-notices/crates/shlex-2.0.1/license-mit +21 -0
  25. package/native-notices/crates/tikv-jemalloc-sys-0.7.1+5.3.1-0-g81034ce1f1373e37dc865038e1bc8eeecf559ce8/license-apache +201 -0
  26. package/native-notices/crates/tikv-jemalloc-sys-0.7.1+5.3.1-0-g81034ce1f1373e37dc865038e1bc8eeecf559ce8/license-mit +25 -0
  27. package/native-notices/crates/tikv-jemallocator-0.7.0/license-apache +201 -0
  28. package/native-notices/crates/tikv-jemallocator-0.7.0/license-mit +25 -0
  29. package/native-notices/manifest.json +135 -1
  30. package/native-notices/native/tikv-jemalloc-sys-0.7.1+5.3.1-0-g81034ce1f1373e37dc865038e1bc8eeecf559ce8/jemalloc-5.3.1/jemalloc-copying +27 -0
  31. package/native-notices/readme.md +5 -3
  32. package/package.json +9 -8
  33. package/readme.md +38 -2
  34. package/thirdparty.md +7 -0
  35. package/ts/00_commitinfo_data.ts +1 -1
  36. package/ts/classes.archiveinspection.ts +245 -0
  37. package/ts/classes.gitmanager.ts +62 -0
  38. package/ts/classes.gitmanagererror.ts +28 -0
  39. package/ts/index.ts +2 -0
  40. package/ts/models.ts +65 -0
  41. package/ts/rust-gitmanager-bridge.ts +48 -1
@@ -0,0 +1,245 @@
1
+ import * as plugins from './plugins.js';
2
+ import { GitManagerError } from './classes.gitmanagererror.js';
3
+ import type {
4
+ IGitManagerArchiveInspection,
5
+ IGitManagerArchiveInspectionRequest,
6
+ } from './models.js';
7
+
8
+ /** The fixed ci-spec path-metadata ceiling of every archive. */
9
+ export const gitManagerArchivePathMetadataCeilingBytes = 134_217_728;
10
+
11
+ const maxInspectorOutputBytes = 64 * 1024;
12
+
13
+ type TInspectorSettlement =
14
+ | { kind: 'closed'; code: number | null; signal: NodeJS.Signals | null }
15
+ | { kind: 'spawnFailed'; error: Error };
16
+
17
+ const assertLimit = (
18
+ nameArg: string,
19
+ valueArg: number,
20
+ minimumArg: number,
21
+ maximumArg: number,
22
+ ): void => {
23
+ if (!Number.isSafeInteger(valueArg) || valueArg < minimumArg || valueArg > maximumArg) {
24
+ throw new TypeError(
25
+ `${nameArg} must be a safe integer between ${minimumArg} and ${maximumArg}.`,
26
+ );
27
+ }
28
+ };
29
+
30
+ const isRecord = (valueArg: unknown): valueArg is Record<string, unknown> =>
31
+ typeof valueArg === 'object' && valueArg !== null && !Array.isArray(valueArg);
32
+
33
+ const isCount = (valueArg: unknown): valueArg is number =>
34
+ typeof valueArg === 'number' && Number.isSafeInteger(valueArg) && valueArg >= 0;
35
+
36
+ /**
37
+ * Parses the inspector's single JSON result: the recomputed metadata of an
38
+ * accepted archive, or a thrown `GitManagerError` for a refused one.
39
+ */
40
+ const parseInspectorResult = (stdoutArg: string): IGitManagerArchiveInspection => {
41
+ let parsed: unknown;
42
+ try {
43
+ parsed = JSON.parse(stdoutArg);
44
+ } catch (error) {
45
+ throw new Error('Archive inspector returned invalid JSON', { cause: error });
46
+ }
47
+ if (isRecord(parsed) && parsed.status === 'rejected' && typeof parsed.message === 'string') {
48
+ if (parsed.reason === 'limitExceeded') {
49
+ throw new GitManagerError('archiveLimitExceeded', parsed.message);
50
+ }
51
+ if (parsed.reason === 'invalid') {
52
+ throw new GitManagerError('archiveRejected', parsed.message);
53
+ }
54
+ }
55
+ if (
56
+ isRecord(parsed)
57
+ && parsed.status === 'accepted'
58
+ && isCount(parsed.sizeBytes)
59
+ && typeof parsed.sha256 === 'string'
60
+ && /^[0-9a-f]{64}$/.test(parsed.sha256)
61
+ && isCount(parsed.extractedSizeBytes)
62
+ && isCount(parsed.entryCount)
63
+ && isCount(parsed.pathMetadataBytes)
64
+ ) {
65
+ return {
66
+ sizeBytes: parsed.sizeBytes,
67
+ sha256: parsed.sha256,
68
+ extractedSizeBytes: parsed.extractedSizeBytes,
69
+ entryCount: parsed.entryCount,
70
+ pathMetadataBytes: parsed.pathMetadataBytes,
71
+ };
72
+ }
73
+ throw new Error('Archive inspector returned an invalid result');
74
+ };
75
+
76
+ /**
77
+ * Streams one archive into a dedicated engine inspector process and returns
78
+ * its recomputed metadata. The inspector reads until the archive is accepted
79
+ * or the first violation; on a refusal the remaining input is not read and
80
+ * the source is closed. Aborting `signalArg` kills the inspector and rejects
81
+ * with the abort reason once the process has exited.
82
+ */
83
+ export const runArchiveInspection = async (
84
+ binaryPathArg: string,
85
+ requestArg: IGitManagerArchiveInspectionRequest,
86
+ signalArg: AbortSignal,
87
+ ): Promise<IGitManagerArchiveInspection> => {
88
+ const maxPathMetadataBytes =
89
+ requestArg.maxPathMetadataBytes ?? gitManagerArchivePathMetadataCeilingBytes;
90
+ assertLimit('maxCompressedBytes', requestArg.maxCompressedBytes, 0, Number.MAX_SAFE_INTEGER);
91
+ assertLimit('maxExtractedBytes', requestArg.maxExtractedBytes, 0, Number.MAX_SAFE_INTEGER);
92
+ assertLimit('maxEntries', requestArg.maxEntries, 0, Number.MAX_SAFE_INTEGER);
93
+ assertLimit(
94
+ 'maxPathMetadataBytes',
95
+ maxPathMetadataBytes,
96
+ 1,
97
+ gitManagerArchivePathMetadataCeilingBytes,
98
+ );
99
+ if (signalArg.aborted) {
100
+ throw signalArg.reason;
101
+ }
102
+
103
+ const child = plugins.childProcess.spawn(binaryPathArg, [
104
+ '--inspect-archive-stream',
105
+ '--max-compressed-bytes',
106
+ String(requestArg.maxCompressedBytes),
107
+ '--max-extracted-bytes',
108
+ String(requestArg.maxExtractedBytes),
109
+ '--max-entries',
110
+ String(requestArg.maxEntries),
111
+ '--max-path-metadata-bytes',
112
+ String(maxPathMetadataBytes),
113
+ ], { stdio: ['pipe', 'pipe', 'pipe'] });
114
+ const killInspector = (): void => {
115
+ if (child.exitCode === null && child.signalCode === null) {
116
+ child.kill('SIGKILL');
117
+ }
118
+ };
119
+
120
+ const settled = new Promise<TInspectorSettlement>((resolve) => {
121
+ child.once('error', (error) => {
122
+ killInspector();
123
+ resolve({ kind: 'spawnFailed', error });
124
+ });
125
+ child.once('close', (code, signal) => resolve({ kind: 'closed', code, signal }));
126
+ });
127
+ let inspectorSettled = false;
128
+ void settled.then(() => {
129
+ inspectorSettled = true;
130
+ });
131
+
132
+ const stdoutChunks: Buffer[] = [];
133
+ const stderrChunks: Buffer[] = [];
134
+ let outputBytes = 0;
135
+ let outputOverflow = false;
136
+ const collect = (chunksArg: Buffer[]) => (chunkArg: Buffer): void => {
137
+ outputBytes += chunkArg.byteLength;
138
+ if (outputBytes > maxInspectorOutputBytes) {
139
+ outputOverflow = true;
140
+ killInspector();
141
+ return;
142
+ }
143
+ chunksArg.push(chunkArg);
144
+ };
145
+ child.stdout.on('data', collect(stdoutChunks));
146
+ child.stderr.on('data', collect(stderrChunks));
147
+
148
+ // A write error is the inspector closing its input after a refusal (or
149
+ // dying); its exit status and result decide the outcome.
150
+ child.stdin.on('error', () => undefined);
151
+
152
+ let aborted = false;
153
+ const abortInspection = (): void => {
154
+ aborted = true;
155
+ killInspector();
156
+ };
157
+ signalArg.addEventListener('abort', abortInspection, { once: true });
158
+
159
+ let delivered = false;
160
+ let sourceFailure: { error: unknown } | undefined;
161
+ const waitForDrain = (): Promise<unknown> => Promise.race([
162
+ new Promise<void>((resolve) => {
163
+ const done = (): void => {
164
+ child.stdin.off('drain', done);
165
+ child.stdin.off('close', done);
166
+ resolve();
167
+ };
168
+ child.stdin.once('drain', done);
169
+ child.stdin.once('close', done);
170
+ }),
171
+ settled,
172
+ ]);
173
+ const feed = async (): Promise<void> => {
174
+ const archive = requestArg.archive;
175
+ if (archive instanceof Uint8Array) {
176
+ child.stdin.end(archive);
177
+ delivered = true;
178
+ return;
179
+ }
180
+ const iterator = archive[Symbol.asyncIterator]();
181
+ let exhausted = false;
182
+ try {
183
+ while (!inspectorSettled) {
184
+ const step = await Promise.race([
185
+ iterator.next().then((result) => ({ kind: 'chunk' as const, result })),
186
+ settled.then(() => ({ kind: 'settled' as const })),
187
+ ]);
188
+ if (step.kind === 'settled' || inspectorSettled) {
189
+ return;
190
+ }
191
+ if (step.result.done) {
192
+ exhausted = true;
193
+ child.stdin.end();
194
+ delivered = true;
195
+ return;
196
+ }
197
+ if (!(step.result.value instanceof Uint8Array)) {
198
+ throw new TypeError('The archive stream must yield Uint8Array chunks.');
199
+ }
200
+ if (!child.stdin.write(step.result.value)) {
201
+ await waitForDrain();
202
+ }
203
+ }
204
+ } catch (error) {
205
+ sourceFailure = { error };
206
+ killInspector();
207
+ } finally {
208
+ if (!exhausted && !sourceFailure) {
209
+ // Close the source like a `for await` loop that stops early; a
210
+ // stalled source must not hold the inspection open.
211
+ void Promise.resolve(iterator.return?.()).catch(() => undefined);
212
+ }
213
+ }
214
+ };
215
+
216
+ try {
217
+ const [settlement] = await Promise.all([settled, feed()]);
218
+ if (aborted) {
219
+ throw signalArg.reason;
220
+ }
221
+ if (sourceFailure) {
222
+ throw sourceFailure.error;
223
+ }
224
+ if (settlement.kind === 'spawnFailed') {
225
+ throw new Error('Failed to spawn the archive inspector', { cause: settlement.error });
226
+ }
227
+ if (outputOverflow) {
228
+ throw new Error('Archive inspector output exceeded its limit');
229
+ }
230
+ if (settlement.code !== 0) {
231
+ const stderr = Buffer.concat(stderrChunks).toString('utf8').trim();
232
+ throw new Error(
233
+ `Archive inspector failed with ${settlement.signal ?? `status ${settlement.code}`}`
234
+ + (stderr ? `: ${stderr}` : ''),
235
+ );
236
+ }
237
+ const inspection = parseInspectorResult(Buffer.concat(stdoutChunks).toString('utf8'));
238
+ if (!delivered) {
239
+ throw new Error('Archive inspector accepted an archive that was not completely delivered');
240
+ }
241
+ return inspection;
242
+ } finally {
243
+ signalArg.removeEventListener('abort', abortInspection);
244
+ }
245
+ };
@@ -1,5 +1,9 @@
1
1
  import * as plugins from './plugins.js';
2
2
  import type {
3
+ IGitManagerAncestryRequest,
4
+ IGitManagerArchiveInspection,
5
+ IGitManagerArchiveInspectionOptions,
6
+ IGitManagerArchiveInspectionRequest,
3
7
  IGitManagerCommitDetail,
4
8
  IGitManagerCommitRequest,
5
9
  IGitManagerCommitSummary,
@@ -41,6 +45,7 @@ import type {
41
45
  } from './models.js';
42
46
  import { RustGitManagerBridge } from './rust-gitmanager-bridge.js';
43
47
  import { GitManagerRepositoryService } from './classes.repositoryservice.js';
48
+ import { runArchiveInspection } from './classes.archiveinspection.js';
44
49
 
45
50
  const readinessDefaultTimeoutMs = 2_000;
46
51
  const readinessMaximumTimeoutMs = 30_000;
@@ -56,6 +61,7 @@ export class GitManager extends plugins.EventEmitter {
56
61
  private stopPromise: Promise<void> | null = null;
57
62
  private config: IRustGitManagerConfig;
58
63
  private readonly repositoryServices = new Set<GitManagerRepositoryService>();
64
+ private readonly archiveInspections = new Map<AbortController, Promise<unknown>>();
59
65
  private lifecycleEpoch = 0;
60
66
  private smartHttpProbeUrl: string | null = null;
61
67
  private readonly activeReadinessControllers = new Set<AbortController>();
@@ -168,12 +174,14 @@ export class GitManager extends plugins.EventEmitter {
168
174
  }
169
175
  if (!this.bridge.running) {
170
176
  await this.closeRepositoryServices();
177
+ await this.abortArchiveInspections();
171
178
  await this.bridge.terminate();
172
179
  this.started = false;
173
180
  return;
174
181
  }
175
182
  try {
176
183
  await this.closeRepositoryServices();
184
+ await this.abortArchiveInspections();
177
185
  await this.bridge.stopGitManager();
178
186
  } finally {
179
187
  await this.bridge.terminate();
@@ -440,6 +448,51 @@ export class GitManager extends plugins.EventEmitter {
440
448
  return this.bridge.readFile(requestArg);
441
449
  }
442
450
 
451
+ /**
452
+ * Answers `git merge-base --is-ancestor` exactly within a work budget.
453
+ * Anything short of an exact answer throws a `GitManagerError`
454
+ * (`invalidObjectId`, `objectNotFound`, `objectNotCommit`,
455
+ * `ancestryBudgetExceeded`); it never degrades to `false`.
456
+ */
457
+ public async isAncestor(requestArg: IGitManagerAncestryRequest): Promise<boolean> {
458
+ this.assertRunning();
459
+ return this.bridge.isAncestor(requestArg);
460
+ }
461
+
462
+ /**
463
+ * Inspects a receiver-side `tar.gz` archive in a dedicated engine process:
464
+ * recomputes its compressed size, SHA-256, extracted bytes, entry count,
465
+ * and path metadata bytes while enforcing the limits and the ci-spec path
466
+ * and link rules. A refused archive throws a `GitManagerError` with code
467
+ * `archiveLimitExceeded` or `archiveRejected`; an invalid limit throws a
468
+ * `TypeError`. `stop()` aborts inspections in flight.
469
+ */
470
+ public async inspectArchive(
471
+ requestArg: IGitManagerArchiveInspectionRequest,
472
+ optionsArg: IGitManagerArchiveInspectionOptions = {},
473
+ ): Promise<IGitManagerArchiveInspection> {
474
+ this.assertRunning();
475
+ const controller = new AbortController();
476
+ const callerSignal = optionsArg.signal;
477
+ const abortForCaller = (): void => {
478
+ controller.abort(callerSignal?.reason);
479
+ };
480
+ if (callerSignal?.aborted) {
481
+ throw callerSignal.reason;
482
+ }
483
+ callerSignal?.addEventListener('abort', abortForCaller, { once: true });
484
+ const inspection = this.bridge.findBinary().then((binaryPath) => {
485
+ return runArchiveInspection(binaryPath, requestArg, controller.signal);
486
+ });
487
+ this.archiveInspections.set(controller, inspection);
488
+ try {
489
+ return await inspection;
490
+ } finally {
491
+ callerSignal?.removeEventListener('abort', abortForCaller);
492
+ this.archiveInspections.delete(controller);
493
+ }
494
+ }
495
+
443
496
  public async listCommits(requestArg: IGitManagerListCommitsRequest): Promise<IGitManagerCommitSummary[]> {
444
497
  this.assertRunning();
445
498
  return this.bridge.listCommits(requestArg);
@@ -675,6 +728,15 @@ export class GitManager extends plugins.EventEmitter {
675
728
  }
676
729
  }
677
730
 
731
+ private async abortArchiveInspections(): Promise<void> {
732
+ const inspections = [...this.archiveInspections];
733
+ this.archiveInspections.clear();
734
+ for (const [controller] of inspections) {
735
+ controller.abort(new Error('GitManager stopped during archive inspection'));
736
+ }
737
+ await Promise.all(inspections.map(([, inspection]) => inspection.catch(() => undefined)));
738
+ }
739
+
678
740
  private async closeRepositoryServices(): Promise<void> {
679
741
  const services = [...this.repositoryServices];
680
742
  this.repositoryServices.clear();
@@ -0,0 +1,28 @@
1
+ import type { TGitManagerErrorCode } from './models.js';
2
+
3
+ const gitManagerErrorCodes: ReadonlySet<string> = new Set<TGitManagerErrorCode>([
4
+ 'invalidObjectId',
5
+ 'objectNotFound',
6
+ 'objectNotCommit',
7
+ 'ancestryBudgetExceeded',
8
+ 'archiveLimitExceeded',
9
+ 'archiveRejected',
10
+ ]);
11
+
12
+ export const isGitManagerErrorCode = (valueArg: unknown): valueArg is TGitManagerErrorCode =>
13
+ typeof valueArg === 'string' && gitManagerErrorCodes.has(valueArg);
14
+
15
+ /**
16
+ * A failure callers can branch on through its stable `code`. Other failures,
17
+ * such as an unknown repository or an engine that is not running, remain
18
+ * plain errors.
19
+ */
20
+ export class GitManagerError extends Error {
21
+ public readonly code: TGitManagerErrorCode;
22
+
23
+ constructor(codeArg: TGitManagerErrorCode, messageArg: string, optionsArg?: ErrorOptions) {
24
+ super(messageArg, optionsArg);
25
+ this.name = 'GitManagerError';
26
+ this.code = codeArg;
27
+ }
28
+ }
package/ts/index.ts CHANGED
@@ -2,3 +2,5 @@ export * from './models.js';
2
2
  export * from './rust-gitmanager-bridge.js';
3
3
  export * from './classes.gitmanager.js';
4
4
  export * from './classes.repositoryservice.js';
5
+ export * from './classes.gitmanagererror.js';
6
+ export { gitManagerArchivePathMetadataCeilingBytes } from './classes.archiveinspection.js';
package/ts/models.ts CHANGED
@@ -89,6 +89,71 @@ export interface IGitManagerRepositoryArchive
89
89
  entryCount: number;
90
90
  }
91
91
 
92
+ /**
93
+ * Asks whether `ancestorObjectId` is reachable from `descendantObjectId`
94
+ * (`git merge-base --is-ancestor`; a commit is its own ancestor). Both must be
95
+ * full lowercase object IDs of commits in the repository; revision
96
+ * expressions, abbreviations, and tag objects are refused.
97
+ */
98
+ export interface IGitManagerAncestryRequest extends IGitManagerRepositorySelector {
99
+ ancestorObjectId: string;
100
+ descendantObjectId: string;
101
+ /**
102
+ * Wall-clock work budget for the object lookup and the history walk,
103
+ * 1 to 60000 ms, default 10000. Exceeding it throws a `GitManagerError`
104
+ * with code `ancestryBudgetExceeded`.
105
+ */
106
+ timeoutMs?: number;
107
+ }
108
+
109
+ /**
110
+ * Limits for inspecting one `tar.gz` archive. Every limit is inclusive.
111
+ */
112
+ export interface IGitManagerArchiveInspectionRequest {
113
+ /** The archive bytes, or a stream of them; it is consumed and closed. */
114
+ archive: Uint8Array | AsyncIterable<Uint8Array>;
115
+ maxCompressedBytes: number;
116
+ maxExtractedBytes: number;
117
+ maxEntries: number;
118
+ /**
119
+ * Ceiling for the UTF-8 bytes of every effective entry path and symlink
120
+ * target, 1 to 134217728 (the fixed ci-spec ceiling, and the default).
121
+ */
122
+ maxPathMetadataBytes?: number;
123
+ }
124
+
125
+ export interface IGitManagerArchiveInspectionOptions {
126
+ signal?: AbortSignal;
127
+ }
128
+
129
+ /** Metadata recomputed from an accepted archive; nothing is taken on trust. */
130
+ export interface IGitManagerArchiveInspection {
131
+ sizeBytes: number;
132
+ sha256: string;
133
+ extractedSizeBytes: number;
134
+ entryCount: number;
135
+ pathMetadataBytes: number;
136
+ }
137
+
138
+ /**
139
+ * Stable codes of `GitManagerError`.
140
+ *
141
+ * - `invalidObjectId`: an object ID is not a full lowercase SHA-1 or SHA-256.
142
+ * - `objectNotFound`: the object does not exist in the repository.
143
+ * - `objectNotCommit`: the object exists but is not a commit.
144
+ * - `ancestryBudgetExceeded`: the ancestry check exceeded `timeoutMs`; the
145
+ * answer is unknown, never `false`.
146
+ * - `archiveLimitExceeded`: the archive exceeds a requested limit.
147
+ * - `archiveRejected`: the archive breaks a format, path, or link rule.
148
+ */
149
+ export type TGitManagerErrorCode =
150
+ | 'invalidObjectId'
151
+ | 'objectNotFound'
152
+ | 'objectNotCommit'
153
+ | 'ancestryBudgetExceeded'
154
+ | 'archiveLimitExceeded'
155
+ | 'archiveRejected';
156
+
92
157
  export interface IGitManagerRepositoryLfsPointer {
93
158
  oid: string;
94
159
  sizeBytes: number;
@@ -1,5 +1,7 @@
1
1
  import * as plugins from './plugins.js';
2
+ import { GitManagerError, isGitManagerErrorCode } from './classes.gitmanagererror.js';
2
3
  import type {
4
+ IGitManagerAncestryRequest,
3
5
  IGitManagerCommitDetail,
4
6
  IGitManagerCommitRequest,
5
7
  IGitManagerCommitSummary,
@@ -74,6 +76,7 @@ type TGitManagerCommands = {
74
76
  mergeRepository: { params: IGitManagerMergeRequest; result: IGitManagerMergeResult };
75
77
  listTree: { params: IGitManagerTreeRequest; result: IGitManagerTreeEntry[] };
76
78
  readFile: { params: IGitManagerReadFileRequest; result: IGitManagerFileContent };
79
+ isAncestor: { params: IGitManagerAncestryRequest; result: boolean };
77
80
  listCommits: { params: IGitManagerListCommitsRequest; result: IGitManagerCommitSummary[] };
78
81
  getCommit: { params: IGitManagerCommitRequest; result: IGitManagerCommitDetail };
79
82
  getDiff: { params: IGitManagerDiffRequest; result: IGitManagerDiff };
@@ -109,16 +112,37 @@ function buildLocalPaths(): string[] {
109
112
  return paths;
110
113
  }
111
114
 
115
+ /**
116
+ * Rethrows a Rust response that carries a stable GitManager error code as a
117
+ * `GitManagerError`; every other failure is rethrown unchanged.
118
+ */
119
+ const rethrowCodedError = (errorArg: unknown): never => {
120
+ if (
121
+ errorArg instanceof plugins.smartrust.RustBridgeRequestError
122
+ && isGitManagerErrorCode(errorArg.responseErrorCode)
123
+ ) {
124
+ throw new GitManagerError(errorArg.responseErrorCode, errorArg.message, {
125
+ cause: errorArg,
126
+ });
127
+ }
128
+ throw errorArg;
129
+ };
130
+
112
131
  export class RustGitManagerBridge extends plugins.EventEmitter {
113
132
  private bridge: plugins.smartrust.RustBridge<TGitManagerCommands>;
133
+ private binaryLocator: plugins.smartrust.RustBinaryLocator;
114
134
 
115
135
  constructor() {
116
136
  super();
117
- this.bridge = new plugins.smartrust.RustBridge<TGitManagerCommands>({
137
+ const binaryLocatorOptions: plugins.smartrust.IBinaryLocatorOptions = {
118
138
  binaryName: 'rustgitmanager',
119
139
  envVarName: 'GITMANAGER_RUST_BINARY',
120
140
  platformPackagePrefix: '@foss.global/gitmanager',
121
141
  localPaths: buildLocalPaths(),
142
+ };
143
+ this.binaryLocator = new plugins.smartrust.RustBinaryLocator(binaryLocatorOptions);
144
+ this.bridge = new plugins.smartrust.RustBridge<TGitManagerCommands>({
145
+ ...binaryLocatorOptions,
122
146
  requestTimeoutMs: 130000,
123
147
  readyTimeoutMs: 10000,
124
148
  });
@@ -135,6 +159,15 @@ export class RustGitManagerBridge extends plugins.EventEmitter {
135
159
  return this.bridge.spawn();
136
160
  }
137
161
 
162
+ /** Resolves the engine binary the bridge spawns, for its one-shot modes. */
163
+ public async findBinary(): Promise<string> {
164
+ const binaryPath = await this.binaryLocator.findBinary();
165
+ if (!binaryPath) {
166
+ throw new Error('Unable to locate the rustgitmanager binary');
167
+ }
168
+ return binaryPath;
169
+ }
170
+
138
171
  public kill(): void {
139
172
  this.bridge.kill();
140
173
  }
@@ -304,6 +337,20 @@ export class RustGitManagerBridge extends plugins.EventEmitter {
304
337
  return this.bridge.sendCommand('readFile', requestArg);
305
338
  }
306
339
 
340
+ public async isAncestor(requestArg: IGitManagerAncestryRequest): Promise<boolean> {
341
+ const result = await this.bridge.sendCommand('isAncestor', {
342
+ org: requestArg.org,
343
+ repo: requestArg.repo,
344
+ ancestorObjectId: requestArg.ancestorObjectId,
345
+ descendantObjectId: requestArg.descendantObjectId,
346
+ ...(requestArg.timeoutMs === undefined ? {} : { timeoutMs: requestArg.timeoutMs }),
347
+ }).catch(rethrowCodedError);
348
+ if (typeof result !== 'boolean') {
349
+ throw new Error('Rust GitManager returned an invalid ancestry result');
350
+ }
351
+ return result;
352
+ }
353
+
307
354
  public async listCommits(requestArg: IGitManagerListCommitsRequest): Promise<IGitManagerCommitSummary[]> {
308
355
  return this.bridge.sendCommand('listCommits', requestArg);
309
356
  }