@foss.global/gitmanager 2.3.0 → 2.5.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.
- package/changelog.md +31 -0
- package/dist_rust/rustgitmanager_linux_amd64 +0 -0
- package/dist_rust/rustgitmanager_linux_amd64.tsrust-build.json +4 -4
- package/dist_rust/rustgitmanager_linux_arm64 +0 -0
- package/dist_rust/rustgitmanager_linux_arm64.tsrust-build.json +4 -4
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.gitmanager.d.ts +90 -7
- package/dist_ts/classes.gitmanager.js +168 -20
- package/dist_ts/classes.gitmanagererror.js +5 -1
- package/dist_ts/classes.repositoryservice.d.ts +9 -0
- package/dist_ts/classes.repositoryservice.js +7 -3
- package/dist_ts/models.d.ts +30 -2
- package/dist_ts/rust-gitmanager-bridge.d.ts +15 -4
- package/dist_ts/rust-gitmanager-bridge.js +55 -12
- package/package.json +2 -2
- package/readme.md +79 -8
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.gitmanager.ts +199 -25
- package/ts/classes.gitmanagererror.ts +4 -0
- package/ts/classes.repositoryservice.ts +13 -0
- package/ts/models.ts +42 -1
- package/ts/rust-gitmanager-bridge.ts +101 -15
package/ts/classes.gitmanager.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
2
|
import type {
|
|
3
3
|
IGitManagerAncestryRequest,
|
|
4
|
+
IGitManagerAtomicPushRequest,
|
|
5
|
+
IGitManagerAtomicPushResult,
|
|
4
6
|
IGitManagerArchiveInspection,
|
|
5
7
|
IGitManagerArchiveInspectionOptions,
|
|
6
8
|
IGitManagerArchiveInspectionRequest,
|
|
@@ -32,6 +34,7 @@ import type {
|
|
|
32
34
|
IGitManagerRepositorySelector,
|
|
33
35
|
IGitManagerRepositorySize,
|
|
34
36
|
IGitManagerRestoreBundleRequest,
|
|
37
|
+
IGitManagerSetDefaultBranchRequest,
|
|
35
38
|
IGitManagerSmartHttpRepositoryAccess,
|
|
36
39
|
IGitManagerSmartHttpRepositoryVisibility,
|
|
37
40
|
IGitManagerStatus,
|
|
@@ -39,6 +42,7 @@ import type {
|
|
|
39
42
|
IGitManagerTagRequest,
|
|
40
43
|
IGitManagerTreeEntry,
|
|
41
44
|
IGitManagerTreeRequest,
|
|
45
|
+
IGitManagerTransferControlOptions,
|
|
42
46
|
IGitManagerOptions,
|
|
43
47
|
IGitManagerReadinessOptions,
|
|
44
48
|
IRustGitManagerConfig,
|
|
@@ -87,6 +91,32 @@ const assertRemoteTransferOptions = (optionsArg: IGitManagerRemoteTransferOption
|
|
|
87
91
|
}
|
|
88
92
|
};
|
|
89
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Checks the shape of push updates before they reach the engine, which
|
|
96
|
+
* answers a malformed request with fixed text only. Ref names and object IDs
|
|
97
|
+
* are validated by the engine.
|
|
98
|
+
*/
|
|
99
|
+
const assertAtomicPushUpdates = (updatesArg: unknown): void => {
|
|
100
|
+
const isObjectIdOrNull = (valueArg: unknown): boolean => valueArg === null || typeof valueArg === 'string';
|
|
101
|
+
if (
|
|
102
|
+
!Array.isArray(updatesArg)
|
|
103
|
+
|| updatesArg.some((update: unknown) => {
|
|
104
|
+
if (update === null || typeof update !== 'object') {
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
const record = update as Record<string, unknown>;
|
|
108
|
+
return typeof record.ref !== 'string'
|
|
109
|
+
|| !isObjectIdOrNull(record.expectedOldObjectId)
|
|
110
|
+
|| !isObjectIdOrNull(record.newObjectId)
|
|
111
|
+
|| Object.keys(record).some((key) => !['ref', 'expectedOldObjectId', 'newObjectId'].includes(key));
|
|
112
|
+
})
|
|
113
|
+
) {
|
|
114
|
+
throw new TypeError(
|
|
115
|
+
'updates must be an array of { ref: string, expectedOldObjectId: string | null, newObjectId: string | null }',
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
90
120
|
type TGitManagerReadinessAbortSource = 'caller' | 'deadline';
|
|
91
121
|
type TGitManagerReadinessProbeOutcome = 'ready' | 'aborted' | 'unavailable';
|
|
92
122
|
|
|
@@ -377,21 +407,90 @@ export class GitManager extends plugins.EventEmitter {
|
|
|
377
407
|
}
|
|
378
408
|
|
|
379
409
|
/**
|
|
380
|
-
* Clones every ref of `remoteUrl` into a new mirror repository
|
|
381
|
-
*
|
|
382
|
-
*
|
|
410
|
+
* Clones every ref of `remoteUrl` into a new mirror repository whose HEAD
|
|
411
|
+
* points at the source's default branch. Pass a credential as
|
|
412
|
+
* `credential`, never inside the URL: the engine hands it to Git only
|
|
413
|
+
* through the process environment and removes it from errors.
|
|
414
|
+
*
|
|
415
|
+
* The import runs alongside other commands and holds `org/repo` until it
|
|
416
|
+
* ends: another mirror transfer of it or a write to it (such as
|
|
417
|
+
* `initBareRepository` or `restoreRepositoryBundle`) fails with
|
|
418
|
+
* `repositoryBusy`. Aborting `optionsArg.signal`, or `stop()`, terminates
|
|
419
|
+
* the clone and removes the partial repository; the call then rejects with
|
|
420
|
+
* `transferAborted`, and no repository is left behind. An import whose
|
|
421
|
+
* clone already completed when the abort arrived returns its result.
|
|
422
|
+
*/
|
|
423
|
+
public async importMirror(
|
|
424
|
+
requestArg: IGitManagerImportMirrorRequest,
|
|
425
|
+
optionsArg: IGitManagerTransferControlOptions = {},
|
|
426
|
+
): Promise<IGitManagerRepositoryInfo> {
|
|
427
|
+
this.assertRunning();
|
|
428
|
+
assertRemoteTransferOptions(requestArg);
|
|
429
|
+
return this.bridge.importMirror(requestArg, optionsArg);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Fetches every ref of an imported mirror's remote again, with the same
|
|
434
|
+
* transfer options as `importMirror`, and points HEAD at the source's
|
|
435
|
+
* current default branch when the source advertises one.
|
|
436
|
+
*
|
|
437
|
+
* The refresh holds `org/repo` like `importMirror`: writes to it, including
|
|
438
|
+
* Smart HTTP pushes, are refused with `repositoryBusy` while it runs, and it
|
|
439
|
+
* is refused while such a write is in progress; reads proceed. Aborting
|
|
440
|
+
* `optionsArg.signal`, or `stop()`, terminates the fetch and rejects with
|
|
441
|
+
* `transferAborted`. The mirror's refs then stay as they were or, when the
|
|
442
|
+
* abort came after `git fetch --atomic` committed its ref transaction, are
|
|
443
|
+
* fully refreshed; HEAD moves only when the refresh completes.
|
|
383
444
|
*/
|
|
384
|
-
public async
|
|
445
|
+
public async refreshMirror(
|
|
446
|
+
requestArg: IGitManagerRefreshMirrorRequest,
|
|
447
|
+
optionsArg: IGitManagerTransferControlOptions = {},
|
|
448
|
+
): Promise<IGitManagerRepositoryInfo> {
|
|
385
449
|
this.assertRunning();
|
|
386
450
|
assertRemoteTransferOptions(requestArg);
|
|
387
|
-
return this.bridge.
|
|
451
|
+
return this.bridge.refreshMirror(requestArg, optionsArg);
|
|
388
452
|
}
|
|
389
453
|
|
|
390
|
-
/**
|
|
391
|
-
|
|
454
|
+
/**
|
|
455
|
+
* Pushes leased ref updates to `remoteUrl` as one atomic transaction: the
|
|
456
|
+
* remote applies all of them or none, however many there are. The remote
|
|
457
|
+
* must advertise the `atomic` receive-pack capability; otherwise a
|
|
458
|
+
* `GitManagerError` with code `atomicPushUnsupported` is thrown and nothing
|
|
459
|
+
* is pushed. A remote that refuses the push request as too large (HTTP 413)
|
|
460
|
+
* throws `remoteLimitExceeded`, and nothing is pushed. `remoteUrl` must
|
|
461
|
+
* be `https`, or `http` to a loopback IP address (`127.0.0.0/8`, `[::1]`;
|
|
462
|
+
* never a host name such as `localhost`), without user information; the
|
|
463
|
+
* credential travels as for `importMirror`. A ref that already holds its
|
|
464
|
+
* `newObjectId` is reported `ok` without its lease being checked; it is not
|
|
465
|
+
* changed, so `ok` never means the lease matched. Any refusal the remote
|
|
466
|
+
* reports per ref returns status `rejected`, with the refused refs `stale`
|
|
467
|
+
* or `rejected` and the up-to-date ones `ok`; the call throws only when the
|
|
468
|
+
* remote's answer does not say what it did.
|
|
469
|
+
*
|
|
470
|
+
* The push only reads the managed repository, so it runs alongside other
|
|
471
|
+
* commands and other transfers. Aborting `optionsArg.signal`, or `stop()`,
|
|
472
|
+
* rejects with `transferAborted`. An abort before the updates are sent (a
|
|
473
|
+
* signal already aborted, or an abort during the local object check or the
|
|
474
|
+
* remote ref listing) changes nothing on the remote. An abort after that
|
|
475
|
+
* terminates the push, and the remote then holds all of the updates or
|
|
476
|
+
* none of them, unknown which: read the remote refs again before retrying.
|
|
477
|
+
*/
|
|
478
|
+
public async pushRefs(
|
|
479
|
+
requestArg: IGitManagerAtomicPushRequest,
|
|
480
|
+
optionsArg: IGitManagerTransferControlOptions = {},
|
|
481
|
+
): Promise<IGitManagerAtomicPushResult> {
|
|
392
482
|
this.assertRunning();
|
|
393
483
|
assertRemoteTransferOptions(requestArg);
|
|
394
|
-
|
|
484
|
+
assertAtomicPushUpdates(requestArg.updates);
|
|
485
|
+
return this.bridge.pushRefs(requestArg, optionsArg);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/** Points the repository's HEAD at an existing branch; a missing branch is refused and HEAD stays unchanged. */
|
|
489
|
+
public async setDefaultBranch(
|
|
490
|
+
requestArg: IGitManagerSetDefaultBranchRequest,
|
|
491
|
+
): Promise<IGitManagerRepositoryInfo> {
|
|
492
|
+
this.assertRunning();
|
|
493
|
+
return this.bridge.setDefaultBranch(requestArg);
|
|
395
494
|
}
|
|
396
495
|
|
|
397
496
|
/** Counts the repository's objects and their exact Git bytes. */
|
|
@@ -420,29 +519,64 @@ export class GitManager extends plugins.EventEmitter {
|
|
|
420
519
|
return this.bridge.createRepositoryArchive(requestArg, optionsArg);
|
|
421
520
|
}
|
|
422
521
|
|
|
522
|
+
/**
|
|
523
|
+
* Starts a bounded `git upload-pack` or protected `git receive-pack` for an
|
|
524
|
+
* authenticated ingress such as SSH.
|
|
525
|
+
*
|
|
526
|
+
* A receive-pack holds its repository against mirror transfers: while its
|
|
527
|
+
* `guardTicketId` is held, `importMirror` and `refreshMirror` of the
|
|
528
|
+
* repository fail with `repositoryBusy`, and a receive-pack is refused with
|
|
529
|
+
* `repositoryBusy` while a mirror transfer runs. Release the ticket with
|
|
530
|
+
* `releaseRepositoryService()` in a `finally` once the process has exited
|
|
531
|
+
* and the push is reconciled; `stop()` releases every ticket. Upload-pack
|
|
532
|
+
* takes no ticket.
|
|
533
|
+
*
|
|
534
|
+
* The ticket ID is chosen here, before the engine is asked. When opening a
|
|
535
|
+
* receive-pack fails, including a bridge timeout after the engine already
|
|
536
|
+
* issued the ticket, the ticket is released before the error is thrown. If
|
|
537
|
+
* that release fails as well, the original error is still thrown: the
|
|
538
|
+
* release error becomes its `cause` when it has none, and otherwise both
|
|
539
|
+
* are thrown together as an `AggregateError` (`errors`: the original error,
|
|
540
|
+
* then the release error; `cause`: the original error).
|
|
541
|
+
*/
|
|
423
542
|
public async openRepositoryService(
|
|
424
543
|
requestArg: IGitManagerRepositoryServiceRequest,
|
|
425
544
|
): Promise<GitManagerRepositoryService> {
|
|
426
545
|
this.assertRunning();
|
|
427
546
|
const serviceEpoch = this.lifecycleEpoch;
|
|
428
|
-
const
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
547
|
+
const guardTicketId = requestArg.service === 'receive-pack'
|
|
548
|
+
? plugins.crypto.randomUUID()
|
|
549
|
+
: undefined;
|
|
550
|
+
let service: GitManagerRepositoryService;
|
|
551
|
+
try {
|
|
552
|
+
const descriptor = await this.bridge.prepareRepositoryService({
|
|
553
|
+
org: requestArg.org,
|
|
554
|
+
repo: requestArg.repo,
|
|
555
|
+
service: requestArg.service,
|
|
556
|
+
...(requestArg.protocol ? { protocol: requestArg.protocol } : {}),
|
|
557
|
+
...(guardTicketId === undefined ? {} : { guardTicketId }),
|
|
558
|
+
});
|
|
559
|
+
if (
|
|
560
|
+
this.lifecycleEpoch !== serviceEpoch
|
|
561
|
+
|| this.stopRequested
|
|
562
|
+
|| !this.running
|
|
563
|
+
) {
|
|
564
|
+
throw new Error('GitManager stopped while preparing repository service');
|
|
565
|
+
}
|
|
566
|
+
if (descriptor.guardTicketId !== guardTicketId) {
|
|
567
|
+
throw new Error('GitManager returned an invalid repository service descriptor');
|
|
568
|
+
}
|
|
569
|
+
service = new GitManagerRepositoryService(
|
|
570
|
+
descriptor,
|
|
571
|
+
requestArg,
|
|
572
|
+
this.config.dataDir,
|
|
573
|
+
);
|
|
574
|
+
} catch (error) {
|
|
575
|
+
if (guardTicketId === undefined) {
|
|
576
|
+
throw error;
|
|
577
|
+
}
|
|
578
|
+
throw await this.releaseUnusedTicket(guardTicketId, error);
|
|
440
579
|
}
|
|
441
|
-
const service = new GitManagerRepositoryService(
|
|
442
|
-
descriptor,
|
|
443
|
-
requestArg,
|
|
444
|
-
this.config.dataDir,
|
|
445
|
-
);
|
|
446
580
|
this.repositoryServices.add(service);
|
|
447
581
|
service.completed.finally(() => {
|
|
448
582
|
this.repositoryServices.delete(service);
|
|
@@ -450,6 +584,46 @@ export class GitManager extends plugins.EventEmitter {
|
|
|
450
584
|
return service;
|
|
451
585
|
}
|
|
452
586
|
|
|
587
|
+
/**
|
|
588
|
+
* Releases the guard ticket of a receive-pack that failed to open and
|
|
589
|
+
* returns the error to throw: `errorArg`, carrying a failed release as its
|
|
590
|
+
* `cause` when it has none, or else an `AggregateError` of both.
|
|
591
|
+
*/
|
|
592
|
+
private async releaseUnusedTicket(ticketIdArg: string, errorArg: unknown): Promise<unknown> {
|
|
593
|
+
try {
|
|
594
|
+
await this.releaseRepositoryService(ticketIdArg);
|
|
595
|
+
return errorArg;
|
|
596
|
+
} catch (releaseError) {
|
|
597
|
+
if (errorArg instanceof Error && errorArg.cause === undefined) {
|
|
598
|
+
errorArg.cause = releaseError;
|
|
599
|
+
return errorArg;
|
|
600
|
+
}
|
|
601
|
+
return new AggregateError(
|
|
602
|
+
[errorArg, releaseError],
|
|
603
|
+
'Opening the repository service failed, and releasing its guard ticket failed',
|
|
604
|
+
{ cause: errorArg },
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Releases the guard ticket of a receive-pack opened with
|
|
611
|
+
* `openRepositoryService()` (`service.guardTicketId`), so mirror transfers
|
|
612
|
+
* of its repository are admitted again. Call it in a `finally` after the
|
|
613
|
+
* Git process has exited. Releasing is idempotent: an unknown or already
|
|
614
|
+
* released ticket, or a stopped engine, whose tickets ended with it, is a
|
|
615
|
+
* no-op.
|
|
616
|
+
*/
|
|
617
|
+
public async releaseRepositoryService(ticketIdArg: string): Promise<void> {
|
|
618
|
+
if (typeof ticketIdArg !== 'string') {
|
|
619
|
+
throw new TypeError('ticketId must be a string');
|
|
620
|
+
}
|
|
621
|
+
if (!this.bridge.running) {
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
await this.bridge.releaseRepositoryService(ticketIdArg);
|
|
625
|
+
}
|
|
626
|
+
|
|
453
627
|
public async restoreRepositoryBundle(
|
|
454
628
|
requestArg: IGitManagerRestoreBundleRequest,
|
|
455
629
|
): Promise<IGitManagerRepositoryInfo> {
|
|
@@ -7,6 +7,10 @@ const gitManagerErrorCodes: ReadonlySet<string> = new Set<TGitManagerErrorCode>(
|
|
|
7
7
|
'ancestryBudgetExceeded',
|
|
8
8
|
'archiveLimitExceeded',
|
|
9
9
|
'archiveRejected',
|
|
10
|
+
'atomicPushUnsupported',
|
|
11
|
+
'remoteLimitExceeded',
|
|
12
|
+
'transferAborted',
|
|
13
|
+
'repositoryBusy',
|
|
10
14
|
]);
|
|
11
15
|
|
|
12
16
|
export const isGitManagerErrorCode = (valueArg: unknown): valueArg is TGitManagerErrorCode =>
|
|
@@ -64,12 +64,15 @@ const validateDescriptor = (
|
|
|
64
64
|
|| !plugins.path.isAbsolute(descriptorArg.hookPath)
|
|
65
65
|
|| !descriptorArg.protectedRefsPolicy
|
|
66
66
|
|| !/^[A-Za-z0-9_-]{1,65536}$/.test(descriptorArg.protectedRefsPolicy)
|
|
67
|
+
|| typeof descriptorArg.guardTicketId !== 'string'
|
|
68
|
+
|| !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(descriptorArg.guardTicketId)
|
|
67
69
|
) {
|
|
68
70
|
throw new Error('GitManager returned an invalid receive-pack descriptor');
|
|
69
71
|
}
|
|
70
72
|
} else if (
|
|
71
73
|
descriptorArg.hookPath !== undefined
|
|
72
74
|
|| descriptorArg.protectedRefsPolicy !== undefined
|
|
75
|
+
|| descriptorArg.guardTicketId !== undefined
|
|
73
76
|
) {
|
|
74
77
|
throw new Error('GitManager returned unexpected upload-pack hook state');
|
|
75
78
|
}
|
|
@@ -80,6 +83,15 @@ export class GitManagerRepositoryService {
|
|
|
80
83
|
public readonly stdout: plugins.stream.Readable;
|
|
81
84
|
public readonly stderr: plugins.stream.Readable;
|
|
82
85
|
public readonly completed: Promise<IGitManagerRepositoryServiceResult>;
|
|
86
|
+
/**
|
|
87
|
+
* Receive-pack only: the guard ticket that holds the repository against
|
|
88
|
+
* mirror transfers (`importMirror`, `refreshMirror`, which fail with
|
|
89
|
+
* `repositoryBusy`) until `GitManager.releaseRepositoryService()` releases
|
|
90
|
+
* it or the engine stops. Release it in a `finally` once the process has
|
|
91
|
+
* exited and the push is reconciled. A UUID that `openRepositoryService()`
|
|
92
|
+
* chose. Null for upload-pack.
|
|
93
|
+
*/
|
|
94
|
+
public readonly guardTicketId: string | null;
|
|
83
95
|
private readonly child: plugins.childProcess.ChildProcessWithoutNullStreams;
|
|
84
96
|
private completionSettled = false;
|
|
85
97
|
private failure: TGitManagerRepositoryServiceFailure | null = null;
|
|
@@ -91,6 +103,7 @@ export class GitManagerRepositoryService {
|
|
|
91
103
|
homeDirectoryArg: string,
|
|
92
104
|
) {
|
|
93
105
|
validateDescriptor(descriptorArg, requestArg);
|
|
106
|
+
this.guardTicketId = descriptorArg.guardTicketId ?? null;
|
|
94
107
|
const args = descriptorArg.service === 'upload-pack'
|
|
95
108
|
? [
|
|
96
109
|
'upload-pack',
|
package/ts/models.ts
CHANGED
|
@@ -79,6 +79,15 @@ export interface IGitManagerRepositoryArchiveOptions {
|
|
|
79
79
|
signal?: AbortSignal;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Call options of the remote transfers `importMirror`, `refreshMirror` and
|
|
84
|
+
* `pushRefs`. Aborting `signal` stops the transfer; the call then rejects
|
|
85
|
+
* with a `GitManagerError` whose code is `transferAborted`.
|
|
86
|
+
*/
|
|
87
|
+
export interface IGitManagerTransferControlOptions {
|
|
88
|
+
signal?: AbortSignal;
|
|
89
|
+
}
|
|
90
|
+
|
|
82
91
|
export interface IGitManagerRepositoryArchive
|
|
83
92
|
extends IGitManagerRepositorySelector {
|
|
84
93
|
objectId: string;
|
|
@@ -145,6 +154,15 @@ export interface IGitManagerArchiveInspection {
|
|
|
145
154
|
* answer is unknown, never `false`.
|
|
146
155
|
* - `archiveLimitExceeded`: the archive exceeds a requested limit.
|
|
147
156
|
* - `archiveRejected`: the archive breaks a format, path, or link rule.
|
|
157
|
+
* - `atomicPushUnsupported`: the push remote does not advertise the `atomic`
|
|
158
|
+
* receive-pack capability; nothing was pushed.
|
|
159
|
+
* - `remoteLimitExceeded`: the push remote refused the push request as too
|
|
160
|
+
* large (HTTP 413); nothing was pushed.
|
|
161
|
+
* - `transferAborted`: a remote transfer was aborted through its `signal` or
|
|
162
|
+
* by `stop()`. An aborted push may or may not have been applied by the
|
|
163
|
+
* remote; read its refs again.
|
|
164
|
+
* - `repositoryBusy`: a mirror transfer holds the repository, or a write to
|
|
165
|
+
* it is in progress when a mirror transfer starts; nothing was changed.
|
|
148
166
|
*/
|
|
149
167
|
export type TGitManagerErrorCode =
|
|
150
168
|
| 'invalidObjectId'
|
|
@@ -152,7 +170,11 @@ export type TGitManagerErrorCode =
|
|
|
152
170
|
| 'objectNotCommit'
|
|
153
171
|
| 'ancestryBudgetExceeded'
|
|
154
172
|
| 'archiveLimitExceeded'
|
|
155
|
-
| 'archiveRejected'
|
|
173
|
+
| 'archiveRejected'
|
|
174
|
+
| 'atomicPushUnsupported'
|
|
175
|
+
| 'remoteLimitExceeded'
|
|
176
|
+
| 'transferAborted'
|
|
177
|
+
| 'repositoryBusy';
|
|
156
178
|
|
|
157
179
|
export interface IGitManagerRepositoryLfsPointer {
|
|
158
180
|
oid: string;
|
|
@@ -169,6 +191,17 @@ export interface IGitManagerRepositoryServiceRequest
|
|
|
169
191
|
protocol?: 'version=0' | 'version=1' | 'version=2';
|
|
170
192
|
}
|
|
171
193
|
|
|
194
|
+
/**
|
|
195
|
+
* The engine request that prepares a repository service. A receive-pack
|
|
196
|
+
* carries the guard ticket ID the facade chose, so the facade can release the
|
|
197
|
+
* ticket even when it never receives the engine's answer.
|
|
198
|
+
*/
|
|
199
|
+
export interface IRustGitManagerRepositoryServicePrepareRequest
|
|
200
|
+
extends IGitManagerRepositoryServiceRequest {
|
|
201
|
+
/** Receive-pack only, and required there: a lowercase hyphenated UUID. */
|
|
202
|
+
guardTicketId?: string;
|
|
203
|
+
}
|
|
204
|
+
|
|
172
205
|
export interface IRustGitManagerRepositoryServiceDescriptor
|
|
173
206
|
extends IGitManagerRepositoryServiceRequest {
|
|
174
207
|
repositoryPath: string;
|
|
@@ -178,6 +211,8 @@ export interface IRustGitManagerRepositoryServiceDescriptor
|
|
|
178
211
|
maxResponseBytes: number;
|
|
179
212
|
processTimeoutMs: number;
|
|
180
213
|
idleTimeoutSeconds: number;
|
|
214
|
+
/** Receive-pack only: the requested guard ticket ID, which `releaseRepositoryService` releases. */
|
|
215
|
+
guardTicketId?: string;
|
|
181
216
|
}
|
|
182
217
|
|
|
183
218
|
/**
|
|
@@ -295,6 +330,10 @@ export type TGitManagerReadinessResult =
|
|
|
295
330
|
};
|
|
296
331
|
|
|
297
332
|
export type {
|
|
333
|
+
IGitManagerAtomicPushRefResult,
|
|
334
|
+
IGitManagerAtomicPushRefUpdate,
|
|
335
|
+
IGitManagerAtomicPushRequest,
|
|
336
|
+
IGitManagerAtomicPushResult,
|
|
298
337
|
IGitManagerCommitDetail,
|
|
299
338
|
IGitManagerCommitRequest,
|
|
300
339
|
IGitManagerCommitSummary,
|
|
@@ -323,10 +362,12 @@ export type {
|
|
|
323
362
|
IGitManagerRepositoryInfo,
|
|
324
363
|
IGitManagerRepositorySelector,
|
|
325
364
|
IGitManagerRepositorySize,
|
|
365
|
+
IGitManagerSetDefaultBranchRequest,
|
|
326
366
|
IGitManagerTag,
|
|
327
367
|
IGitManagerTagRequest,
|
|
328
368
|
IGitManagerTreeEntry,
|
|
329
369
|
IGitManagerTreeRequest,
|
|
370
|
+
TGitManagerAtomicPushRefStatus,
|
|
330
371
|
TGitManagerDiffFileStatus,
|
|
331
372
|
TGitManagerDiffLineKind,
|
|
332
373
|
TGitManagerMergeMethod,
|
|
@@ -2,6 +2,8 @@ import * as plugins from './plugins.js';
|
|
|
2
2
|
import { GitManagerError, isGitManagerErrorCode } from './classes.gitmanagererror.js';
|
|
3
3
|
import type {
|
|
4
4
|
IGitManagerAncestryRequest,
|
|
5
|
+
IGitManagerAtomicPushRequest,
|
|
6
|
+
IGitManagerAtomicPushResult,
|
|
5
7
|
IGitManagerCommitDetail,
|
|
6
8
|
IGitManagerCommitRequest,
|
|
7
9
|
IGitManagerCommitSummary,
|
|
@@ -24,11 +26,11 @@ import type {
|
|
|
24
26
|
IGitManagerRepositoryArchiveOptions,
|
|
25
27
|
IGitManagerRepositoryArchiveRequest,
|
|
26
28
|
IGitManagerRepositoryLfsPointer,
|
|
27
|
-
IGitManagerRepositoryServiceRequest,
|
|
28
29
|
IGitManagerRepositoryInfo,
|
|
29
30
|
IGitManagerRepositorySelector,
|
|
30
31
|
IGitManagerRepositorySize,
|
|
31
32
|
IGitManagerRestoreBundleRequest,
|
|
33
|
+
IGitManagerSetDefaultBranchRequest,
|
|
32
34
|
IGitManagerSmartHttpRepositoryAccess,
|
|
33
35
|
IGitManagerSmartHttpRepositoryVisibility,
|
|
34
36
|
IGitManagerStatus,
|
|
@@ -36,9 +38,11 @@ import type {
|
|
|
36
38
|
IGitManagerTagRequest,
|
|
37
39
|
IGitManagerTreeEntry,
|
|
38
40
|
IGitManagerTreeRequest,
|
|
41
|
+
IGitManagerTransferControlOptions,
|
|
39
42
|
IRustGitManagerConfig,
|
|
40
43
|
TGitManagerRefInventoryResult,
|
|
41
44
|
IRustGitManagerRepositoryServiceDescriptor,
|
|
45
|
+
IRustGitManagerRepositoryServicePrepareRequest,
|
|
42
46
|
} from './models.js';
|
|
43
47
|
|
|
44
48
|
type TRepositoryArchiveCancelStatus =
|
|
@@ -56,6 +60,14 @@ interface IRepositoryArchiveCancelRequest {
|
|
|
56
60
|
operationId: string;
|
|
57
61
|
}
|
|
58
62
|
|
|
63
|
+
/** A remote transfer under the operation ID that `cancelTransfer` names. */
|
|
64
|
+
interface ITransferOperationRequest<TRequest> {
|
|
65
|
+
operationId: string;
|
|
66
|
+
request: TRequest;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
type TTransferCancelStatus = 'cancelling' | 'not_found';
|
|
70
|
+
|
|
59
71
|
type TGitManagerCommands = {
|
|
60
72
|
start: { params: { config: IRustGitManagerConfig }; result: Record<string, never> };
|
|
61
73
|
stop: { params: Record<string, never>; result: Record<string, never> };
|
|
@@ -64,14 +76,27 @@ type TGitManagerCommands = {
|
|
|
64
76
|
setSmartHttpRepositoryVisibility: { params: IGitManagerSmartHttpRepositoryVisibility; result: Record<string, never> };
|
|
65
77
|
setSmartHttpRepositoryProtectedRefs: { params: IGitManagerSmartHttpRepositoryProtectedRefs; result: IGitManagerProtectedRefsApplyResult };
|
|
66
78
|
initBareRepository: { params: IGitManagerInitBareRepositoryRequest; result: IGitManagerRepositoryInfo };
|
|
67
|
-
importMirror: {
|
|
68
|
-
|
|
79
|
+
importMirror: {
|
|
80
|
+
params: ITransferOperationRequest<IGitManagerImportMirrorRequest>;
|
|
81
|
+
result: IGitManagerRepositoryInfo;
|
|
82
|
+
};
|
|
83
|
+
refreshMirror: {
|
|
84
|
+
params: ITransferOperationRequest<IGitManagerRefreshMirrorRequest>;
|
|
85
|
+
result: IGitManagerRepositoryInfo;
|
|
86
|
+
};
|
|
87
|
+
pushRefs: {
|
|
88
|
+
params: ITransferOperationRequest<IGitManagerAtomicPushRequest>;
|
|
89
|
+
result: IGitManagerAtomicPushResult;
|
|
90
|
+
};
|
|
91
|
+
cancelTransfer: { params: { operationId: string }; result: { status: TTransferCancelStatus } };
|
|
92
|
+
setDefaultBranch: { params: IGitManagerSetDefaultBranchRequest; result: IGitManagerRepositoryInfo };
|
|
69
93
|
verifyRepository: { params: IGitManagerRepositorySelector; result: IGitManagerRepositoryInfo };
|
|
70
94
|
getRepositorySize: { params: IGitManagerRepositorySelector; result: IGitManagerRepositorySize };
|
|
71
95
|
createRepositoryBundle: { params: IGitManagerRepositorySelector; result: IGitManagerRepositoryBundle };
|
|
72
96
|
createRepositoryArchive: { params: IRepositoryArchiveOperationRequest; result: IGitManagerRepositoryArchive };
|
|
73
97
|
cancelRepositoryArchive: { params: IRepositoryArchiveCancelRequest; result: { status: TRepositoryArchiveCancelStatus } };
|
|
74
|
-
prepareRepositoryService: { params:
|
|
98
|
+
prepareRepositoryService: { params: IRustGitManagerRepositoryServicePrepareRequest; result: IRustGitManagerRepositoryServiceDescriptor };
|
|
99
|
+
releaseRepositoryService: { params: { ticketId: string }; result: { released: boolean } };
|
|
75
100
|
restoreRepositoryBundle: { params: IGitManagerRestoreBundleRequest; result: IGitManagerRepositoryInfo };
|
|
76
101
|
listRepositories: { params: Record<string, never>; result: IGitManagerRepositoryInfo[] };
|
|
77
102
|
listRefs: { params: IGitManagerRefRequest; result: TGitManagerRefInventoryResult };
|
|
@@ -97,6 +122,9 @@ const defaultRemoteTransferTimeoutMs = 120_000;
|
|
|
97
122
|
const remoteTransferResponseTimeoutMs = (timeoutMsArg: number | undefined): number =>
|
|
98
123
|
(timeoutMsArg ?? defaultRemoteTransferTimeoutMs) + 30_000;
|
|
99
124
|
|
|
125
|
+
/** The cancel command is answered at once; the transfer reports its own end. */
|
|
126
|
+
const transferCancelTimeoutMs = 30_000;
|
|
127
|
+
|
|
100
128
|
function getPackageRoot(): string {
|
|
101
129
|
const thisDir = plugins.path.dirname(plugins.url.fileURLToPath(import.meta.url));
|
|
102
130
|
return plugins.path.resolve(thisDir, '..');
|
|
@@ -227,19 +255,72 @@ export class RustGitManagerBridge extends plugins.EventEmitter {
|
|
|
227
255
|
}
|
|
228
256
|
|
|
229
257
|
public async initBareRepository(requestArg: IGitManagerInitBareRepositoryRequest): Promise<IGitManagerRepositoryInfo> {
|
|
230
|
-
return this.bridge.sendCommand('initBareRepository', requestArg);
|
|
258
|
+
return this.bridge.sendCommand('initBareRepository', requestArg).catch(rethrowCodedError);
|
|
231
259
|
}
|
|
232
260
|
|
|
233
|
-
public async importMirror(
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
261
|
+
public async importMirror(
|
|
262
|
+
requestArg: IGitManagerImportMirrorRequest,
|
|
263
|
+
optionsArg: IGitManagerTransferControlOptions = {},
|
|
264
|
+
): Promise<IGitManagerRepositoryInfo> {
|
|
265
|
+
return this.runTransfer('importMirror', requestArg, optionsArg);
|
|
237
266
|
}
|
|
238
267
|
|
|
239
|
-
public async refreshMirror(
|
|
240
|
-
|
|
268
|
+
public async refreshMirror(
|
|
269
|
+
requestArg: IGitManagerRefreshMirrorRequest,
|
|
270
|
+
optionsArg: IGitManagerTransferControlOptions = {},
|
|
271
|
+
): Promise<IGitManagerRepositoryInfo> {
|
|
272
|
+
return this.runTransfer('refreshMirror', requestArg, optionsArg);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
public async pushRefs(
|
|
276
|
+
requestArg: IGitManagerAtomicPushRequest,
|
|
277
|
+
optionsArg: IGitManagerTransferControlOptions = {},
|
|
278
|
+
): Promise<IGitManagerAtomicPushResult> {
|
|
279
|
+
return this.runTransfer('pushRefs', requestArg, optionsArg);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Runs a remote transfer under a fresh operation ID. Aborting the signal
|
|
284
|
+
* sends `cancelTransfer`; the engine then terminates the transfer's Git
|
|
285
|
+
* process and answers the transfer itself with `transferAborted`. A signal
|
|
286
|
+
* that is already aborted rejects before anything is sent.
|
|
287
|
+
*/
|
|
288
|
+
private async runTransfer<TMethod extends 'importMirror' | 'refreshMirror' | 'pushRefs'>(
|
|
289
|
+
methodArg: TMethod,
|
|
290
|
+
requestArg: TGitManagerCommands[TMethod]['params']['request'],
|
|
291
|
+
optionsArg: IGitManagerTransferControlOptions,
|
|
292
|
+
): Promise<TGitManagerCommands[TMethod]['result']> {
|
|
293
|
+
const signal = optionsArg.signal;
|
|
294
|
+
if (signal?.aborted) {
|
|
295
|
+
throw new GitManagerError('transferAborted', 'The transfer was aborted before it started', {
|
|
296
|
+
cause: signal.reason,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
const operationId = plugins.crypto.randomUUID();
|
|
300
|
+
const params = { operationId, request: requestArg } as TGitManagerCommands[TMethod]['params'];
|
|
301
|
+
const transfer = this.bridge.sendCommand(methodArg, params, {
|
|
241
302
|
timeoutMs: remoteTransferResponseTimeoutMs(requestArg.timeoutMs),
|
|
242
303
|
});
|
|
304
|
+
const cancelTransfer = (): void => {
|
|
305
|
+
// Only the transfer's own response settles the call.
|
|
306
|
+
void this.bridge.sendCommand('cancelTransfer', { operationId }, {
|
|
307
|
+
timeoutMs: transferCancelTimeoutMs,
|
|
308
|
+
}).catch(() => undefined);
|
|
309
|
+
};
|
|
310
|
+
signal?.addEventListener('abort', cancelTransfer, { once: true });
|
|
311
|
+
try {
|
|
312
|
+
return await transfer;
|
|
313
|
+
} catch (error) {
|
|
314
|
+
return rethrowCodedError(error);
|
|
315
|
+
} finally {
|
|
316
|
+
signal?.removeEventListener('abort', cancelTransfer);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
public async setDefaultBranch(
|
|
321
|
+
requestArg: IGitManagerSetDefaultBranchRequest,
|
|
322
|
+
): Promise<IGitManagerRepositoryInfo> {
|
|
323
|
+
return this.bridge.sendCommand('setDefaultBranch', requestArg).catch(rethrowCodedError);
|
|
243
324
|
}
|
|
244
325
|
|
|
245
326
|
public async getRepositorySize(requestArg: IGitManagerRepositorySelector): Promise<IGitManagerRepositorySize> {
|
|
@@ -320,15 +401,20 @@ export class RustGitManagerBridge extends plugins.EventEmitter {
|
|
|
320
401
|
}
|
|
321
402
|
|
|
322
403
|
public async prepareRepositoryService(
|
|
323
|
-
requestArg:
|
|
404
|
+
requestArg: IRustGitManagerRepositoryServicePrepareRequest,
|
|
324
405
|
): Promise<IRustGitManagerRepositoryServiceDescriptor> {
|
|
325
|
-
return this.bridge.sendCommand('prepareRepositoryService', requestArg);
|
|
406
|
+
return this.bridge.sendCommand('prepareRepositoryService', requestArg).catch(rethrowCodedError);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/** Releases a receive-pack guard ticket; unknown or released tickets are a no-op. */
|
|
410
|
+
public async releaseRepositoryService(ticketIdArg: string): Promise<void> {
|
|
411
|
+
await this.bridge.sendCommand('releaseRepositoryService', { ticketId: ticketIdArg });
|
|
326
412
|
}
|
|
327
413
|
|
|
328
414
|
public async restoreRepositoryBundle(
|
|
329
415
|
requestArg: IGitManagerRestoreBundleRequest,
|
|
330
416
|
): Promise<IGitManagerRepositoryInfo> {
|
|
331
|
-
return this.bridge.sendCommand('restoreRepositoryBundle', requestArg);
|
|
417
|
+
return this.bridge.sendCommand('restoreRepositoryBundle', requestArg).catch(rethrowCodedError);
|
|
332
418
|
}
|
|
333
419
|
|
|
334
420
|
public async listRepositories(): Promise<IGitManagerRepositoryInfo[]> {
|
|
@@ -350,7 +436,7 @@ export class RustGitManagerBridge extends plugins.EventEmitter {
|
|
|
350
436
|
public async mergeRepository(
|
|
351
437
|
requestArg: IGitManagerMergeRequest,
|
|
352
438
|
): Promise<IGitManagerMergeResult> {
|
|
353
|
-
return this.bridge.sendCommand('mergeRepository', requestArg);
|
|
439
|
+
return this.bridge.sendCommand('mergeRepository', requestArg).catch(rethrowCodedError);
|
|
354
440
|
}
|
|
355
441
|
|
|
356
442
|
public async listTree(requestArg: IGitManagerTreeRequest): Promise<IGitManagerTreeEntry[]> {
|