@davideasden/pi-undo 0.2.17 → 0.2.18
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/package.json +2 -1
- package/src/durable-pack.ts +7 -2
- package/src/restore-engine.ts +125 -33
- package/src/snapshot-store.ts +95 -1
package/package.json
CHANGED
package/src/durable-pack.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { randomBytes } from "node:crypto";
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
2
|
import { copyFile, link, lstat, open, readFile, rename, rm, type FileHandle } from "node:fs/promises";
|
|
3
3
|
import { dirname, join, resolve } from "node:path";
|
|
4
4
|
|
|
@@ -194,7 +194,12 @@ export async function createDurablePack(
|
|
|
194
194
|
await rm(temporary, { force: true }).catch(() => {});
|
|
195
195
|
throw error;
|
|
196
196
|
}
|
|
197
|
-
const
|
|
197
|
+
const digest = createHash("sha256");
|
|
198
|
+
digest.update(MAGIC);
|
|
199
|
+
digest.update(lengthBytes);
|
|
200
|
+
digest.update(headerBytes);
|
|
201
|
+
for (const payload of payloads) digest.update(payload);
|
|
202
|
+
const packChecksum = digest.digest("hex");
|
|
198
203
|
return durablePackFromInput(input.opId, input.planDigest, packPath, packChecksum, entries);
|
|
199
204
|
}
|
|
200
205
|
|
package/src/restore-engine.ts
CHANGED
|
@@ -38,6 +38,7 @@ const PREPARED_PLAN_CACHE_LIMIT = 16;
|
|
|
38
38
|
const RESTORE_FILE_BATCH_MAX_ENTRIES = 1_024;
|
|
39
39
|
const RESTORE_FILE_BATCH_MAX_BYTES = 64 * 1024 * 1024;
|
|
40
40
|
const RESTORE_FILE_PREPARE_CONCURRENCY = 32;
|
|
41
|
+
const RESTORE_FILE_VERIFY_CONCURRENCY = 32;
|
|
41
42
|
|
|
42
43
|
export interface RestorePlan {
|
|
43
44
|
currentManifestId: ManifestId;
|
|
@@ -519,7 +520,13 @@ export class RestoreEngine {
|
|
|
519
520
|
return { code: "recovery_required", verifiedPaths: 0, totalPaths: currentPaths.size };
|
|
520
521
|
}
|
|
521
522
|
try {
|
|
522
|
-
await this.assertCompleteVisibleSubset(
|
|
523
|
+
await this.assertCompleteVisibleSubset(
|
|
524
|
+
topologyBefore,
|
|
525
|
+
[current, target],
|
|
526
|
+
options.mutationJournal,
|
|
527
|
+
[],
|
|
528
|
+
this.completeCoverageOwnedPaths(plan.scopePaths, [currentPaths, targetPaths]),
|
|
529
|
+
);
|
|
523
530
|
} catch {
|
|
524
531
|
return { code: "restore_failed_safe", verifiedPaths: 0, totalPaths: 0 };
|
|
525
532
|
}
|
|
@@ -588,6 +595,11 @@ export class RestoreEngine {
|
|
|
588
595
|
);
|
|
589
596
|
return result;
|
|
590
597
|
}
|
|
598
|
+
try {
|
|
599
|
+
await this.prefetchCompleteRestoreBlobs(plan, current, target, currentPaths, targetPaths);
|
|
600
|
+
} catch {
|
|
601
|
+
// 预取是性能优化;失败时继续走原有逐文件校验和可恢复 mutation 路径。
|
|
602
|
+
}
|
|
591
603
|
const preflight = await this.verifyKnownState(current, target, currentPaths, targetPaths, plan.scopePaths);
|
|
592
604
|
if (!preflight.ok) {
|
|
593
605
|
return {
|
|
@@ -625,7 +637,13 @@ export class RestoreEngine {
|
|
|
625
637
|
|
|
626
638
|
const topologyAfter = await this.discovery.discover(this.workspaceRoot);
|
|
627
639
|
assertUnchangedTopology(topologyBefore, topologyAfter);
|
|
628
|
-
await this.assertCompleteVisibleSubset(
|
|
640
|
+
await this.assertCompleteVisibleSubset(
|
|
641
|
+
topologyAfter,
|
|
642
|
+
[target],
|
|
643
|
+
options.mutationJournal,
|
|
644
|
+
[],
|
|
645
|
+
this.completeCoverageOwnedPaths(plan.scopePaths, [targetPaths]),
|
|
646
|
+
);
|
|
629
647
|
const verification = await this.verifyTarget(
|
|
630
648
|
target,
|
|
631
649
|
currentPaths,
|
|
@@ -702,6 +720,7 @@ export class RestoreEngine {
|
|
|
702
720
|
? []
|
|
703
721
|
: [artifacts.source, ...(artifacts.target === null ? [] : [artifacts.target])];
|
|
704
722
|
}),
|
|
723
|
+
this.completeCoverageOwnedPaths(plan.scopePaths, [targetPaths]),
|
|
705
724
|
);
|
|
706
725
|
const totalPaths = plan.deletePaths.length + plan.writePaths.length;
|
|
707
726
|
if ((await options.mutationJournal.load()).length !== 0) {
|
|
@@ -887,6 +906,41 @@ export class RestoreEngine {
|
|
|
887
906
|
return result;
|
|
888
907
|
}
|
|
889
908
|
|
|
909
|
+
private completeCoverageOwnedPaths(
|
|
910
|
+
scopePaths: readonly string[] | undefined,
|
|
911
|
+
ownedPaths: readonly ReadonlyMap<string, OwnedPath>[],
|
|
912
|
+
): readonly ReadonlyMap<string, OwnedPath>[] | undefined {
|
|
913
|
+
return scopePaths === undefined ? ownedPaths : undefined;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
private async prefetchCompleteRestoreBlobs(
|
|
917
|
+
plan: RestorePlan,
|
|
918
|
+
current: SnapshotManifest,
|
|
919
|
+
target: SnapshotManifest,
|
|
920
|
+
currentPaths: ReadonlyMap<string, OwnedPath>,
|
|
921
|
+
targetPaths: ReadonlyMap<string, OwnedPath>,
|
|
922
|
+
): Promise<void> {
|
|
923
|
+
if (plan.scopePaths !== undefined || !SnapshotStore.supportsValidatedBlobBatch(this.store)) return;
|
|
924
|
+
const requestsFor = (paths: ReadonlyMap<string, OwnedPath>, extraPaths: readonly string[] = []) => {
|
|
925
|
+
const requests = [];
|
|
926
|
+
const requested = new Set([...plan.writePaths, ...extraPaths]);
|
|
927
|
+
for (const path of requested) {
|
|
928
|
+
const owned = paths.get(path);
|
|
929
|
+
if (owned === undefined || owned.entry.kind !== "file" || owned.entry.blobId === null) continue;
|
|
930
|
+
requests.push({
|
|
931
|
+
rootPath: owned.root.relativeRoot,
|
|
932
|
+
blobId: owned.entry.blobId,
|
|
933
|
+
relativePath: owned.entry.relativePath,
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
return requests;
|
|
937
|
+
};
|
|
938
|
+
await Promise.all([
|
|
939
|
+
this.store.prefetchBlobs(current.manifestId, requestsFor(currentPaths, plan.deletePaths)),
|
|
940
|
+
this.store.prefetchBlobs(target.manifestId, requestsFor(targetPaths)),
|
|
941
|
+
]);
|
|
942
|
+
}
|
|
943
|
+
|
|
890
944
|
private assertCurrentTopology(
|
|
891
945
|
current: SnapshotManifest,
|
|
892
946
|
target: SnapshotManifest,
|
|
@@ -926,12 +980,13 @@ export class RestoreEngine {
|
|
|
926
980
|
const paths = [...new Set([...currentPaths.keys(), ...targetPaths.keys()])]
|
|
927
981
|
.filter((path) => scope === undefined || scope.has(path))
|
|
928
982
|
.sort(comparePaths);
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
983
|
+
const results: Array<boolean | undefined> = new Array(paths.length);
|
|
984
|
+
let nextIndex = 0;
|
|
985
|
+
let stop = false;
|
|
986
|
+
let failure: unknown;
|
|
987
|
+
let failureIndex: number | undefined;
|
|
988
|
+
const verifyPath = async (path: string): Promise<boolean> => {
|
|
989
|
+
if (await this.pathIsShadowedByTarget(target.manifestId, path, targetPaths)) return true;
|
|
935
990
|
const currentPath = currentPaths.get(path);
|
|
936
991
|
const targetPath = targetPaths.get(path);
|
|
937
992
|
const matchesCurrent = currentPath !== undefined &&
|
|
@@ -941,7 +996,37 @@ export class RestoreEngine {
|
|
|
941
996
|
const matchesAbsentSide = !matchesCurrent && !matchesTarget &&
|
|
942
997
|
(currentPath === undefined || targetPath === undefined) &&
|
|
943
998
|
await this.pathIsAbsent(path);
|
|
944
|
-
|
|
999
|
+
return matchesCurrent || matchesTarget || matchesAbsentSide;
|
|
1000
|
+
};
|
|
1001
|
+
const worker = async (): Promise<void> => {
|
|
1002
|
+
while (!stop && nextIndex < paths.length) {
|
|
1003
|
+
const index = nextIndex;
|
|
1004
|
+
nextIndex += 1;
|
|
1005
|
+
try {
|
|
1006
|
+
const ok = await verifyPath(paths[index]!);
|
|
1007
|
+
results[index] = ok;
|
|
1008
|
+
if (!ok) stop = true;
|
|
1009
|
+
} catch (error) {
|
|
1010
|
+
if (failureIndex === undefined || index < failureIndex) {
|
|
1011
|
+
failure = error;
|
|
1012
|
+
failureIndex = index;
|
|
1013
|
+
}
|
|
1014
|
+
stop = true;
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
};
|
|
1018
|
+
if (paths.length > 0) {
|
|
1019
|
+
await Promise.all(Array.from(
|
|
1020
|
+
{ length: Math.min(RESTORE_FILE_VERIFY_CONCURRENCY, paths.length) },
|
|
1021
|
+
() => worker(),
|
|
1022
|
+
));
|
|
1023
|
+
}
|
|
1024
|
+
let verifiedPaths = 0;
|
|
1025
|
+
for (let index = 0; index < results.length; index += 1) {
|
|
1026
|
+
const ok = results[index];
|
|
1027
|
+
if (ok === false) return { ok: false, verifiedPaths, totalPaths: paths.length };
|
|
1028
|
+
if (ok === undefined) {
|
|
1029
|
+
if (failureIndex === index && failure !== undefined) throw failure;
|
|
945
1030
|
return { ok: false, verifiedPaths, totalPaths: paths.length };
|
|
946
1031
|
}
|
|
947
1032
|
verifiedPaths += 1;
|
|
@@ -1168,7 +1253,13 @@ export class RestoreEngine {
|
|
|
1168
1253
|
await this.writePlannedPaths(current.manifestId, currentPaths, rollbackPlan.writePaths, context);
|
|
1169
1254
|
const topologyAfter = await this.discovery.discover(this.workspaceRoot);
|
|
1170
1255
|
assertUnchangedTopology(topologyBefore, topologyAfter);
|
|
1171
|
-
await this.assertCompleteVisibleSubset(
|
|
1256
|
+
await this.assertCompleteVisibleSubset(
|
|
1257
|
+
topologyAfter,
|
|
1258
|
+
[current],
|
|
1259
|
+
options.mutationJournal,
|
|
1260
|
+
[],
|
|
1261
|
+
this.completeCoverageOwnedPaths(scopePaths, [currentPaths]),
|
|
1262
|
+
);
|
|
1172
1263
|
const verification = await this.verifyTarget(
|
|
1173
1264
|
current,
|
|
1174
1265
|
targetPaths,
|
|
@@ -1205,7 +1296,13 @@ export class RestoreEngine {
|
|
|
1205
1296
|
try {
|
|
1206
1297
|
const topologyAfter = await this.discovery.discover(this.workspaceRoot);
|
|
1207
1298
|
assertUnchangedTopology(topologyBefore, topologyAfter);
|
|
1208
|
-
await this.assertCompleteVisibleSubset(
|
|
1299
|
+
await this.assertCompleteVisibleSubset(
|
|
1300
|
+
topologyAfter,
|
|
1301
|
+
[current],
|
|
1302
|
+
options.mutationJournal,
|
|
1303
|
+
[],
|
|
1304
|
+
this.completeCoverageOwnedPaths(scopePaths, [currentPaths]),
|
|
1305
|
+
);
|
|
1209
1306
|
const verification = await this.verifyTarget(
|
|
1210
1307
|
current,
|
|
1211
1308
|
targetPaths,
|
|
@@ -1397,16 +1494,17 @@ export class RestoreEngine {
|
|
|
1397
1494
|
allowedManifests: readonly SnapshotManifest[],
|
|
1398
1495
|
mutationJournal?: MutationJournal,
|
|
1399
1496
|
extraExclusions: readonly string[] = [],
|
|
1497
|
+
ownedPaths?: readonly (ReadonlyMap<string, OwnedPath> | undefined)[],
|
|
1400
1498
|
): Promise<void> {
|
|
1401
1499
|
if (allowedManifests.some((manifest) => manifest.coverage !== "complete")) {
|
|
1402
1500
|
return;
|
|
1403
1501
|
}
|
|
1404
1502
|
const allowedPaths = new Set<string>();
|
|
1405
|
-
for (const manifest of allowedManifests) {
|
|
1503
|
+
for (const [index, manifest] of allowedManifests.entries()) {
|
|
1406
1504
|
for (const path of ignoredWorkspacePaths(manifest)) {
|
|
1407
1505
|
allowedPaths.add(path);
|
|
1408
1506
|
}
|
|
1409
|
-
const paths = await this.readOwnedPaths(manifest);
|
|
1507
|
+
const paths = ownedPaths?.[index] ?? await this.readOwnedPaths(manifest);
|
|
1410
1508
|
for (const [path, owned] of paths) {
|
|
1411
1509
|
if (owned.entry.kind !== "directory") {
|
|
1412
1510
|
allowedPaths.add(path);
|
|
@@ -1435,23 +1533,18 @@ export class RestoreEngine {
|
|
|
1435
1533
|
deletePaths: readonly string[],
|
|
1436
1534
|
scopePaths?: readonly string[],
|
|
1437
1535
|
): Promise<{ verifiedPaths: number; totalPaths: number; pathFingerprints: string[] }> {
|
|
1438
|
-
const pathFingerprints: string[] = [];
|
|
1439
1536
|
const scope = scopePaths === undefined ? undefined : new Set(scopePaths);
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
) {
|
|
1452
|
-
continue;
|
|
1453
|
-
}
|
|
1454
|
-
totalPaths += 1;
|
|
1537
|
+
const scopedTargets = [...targetPaths].filter(([path]) => scope === undefined || scope.has(path));
|
|
1538
|
+
const pathFingerprints = await mapConcurrentOrdered(
|
|
1539
|
+
scopedTargets,
|
|
1540
|
+
RESTORE_FILE_VERIFY_CONCURRENCY,
|
|
1541
|
+
([, owned]) => this.verifyEntry(target.manifestId, owned),
|
|
1542
|
+
);
|
|
1543
|
+
const remainingDeletes = deletePaths.filter((path) =>
|
|
1544
|
+
!targetPaths.has(path) &&
|
|
1545
|
+
currentPaths.get(path)?.entry.kind !== "directory" &&
|
|
1546
|
+
!hasNonDirectoryAncestor(path, targetPaths));
|
|
1547
|
+
await mapConcurrentOrdered(remainingDeletes, RESTORE_FILE_VERIFY_CONCURRENCY, async (path) => {
|
|
1455
1548
|
try {
|
|
1456
1549
|
await lstat(this.absolutePath(path));
|
|
1457
1550
|
throw new Error(`目标应删除的路径仍然存在:${path}`);
|
|
@@ -1460,11 +1553,10 @@ export class RestoreEngine {
|
|
|
1460
1553
|
throw error;
|
|
1461
1554
|
}
|
|
1462
1555
|
}
|
|
1463
|
-
|
|
1464
|
-
}
|
|
1556
|
+
});
|
|
1465
1557
|
return {
|
|
1466
|
-
verifiedPaths,
|
|
1467
|
-
totalPaths,
|
|
1558
|
+
verifiedPaths: pathFingerprints.length + remainingDeletes.length,
|
|
1559
|
+
totalPaths: pathFingerprints.length + remainingDeletes.length,
|
|
1468
1560
|
pathFingerprints,
|
|
1469
1561
|
};
|
|
1470
1562
|
}
|
package/src/snapshot-store.ts
CHANGED
|
@@ -210,6 +210,8 @@ export class SnapshotStore {
|
|
|
210
210
|
private readonly blobCache = new Map<string, CachedBlob>();
|
|
211
211
|
private readonly visibleLeafCache = new Map<string, Map<string, CachedVisibleLeaf>>();
|
|
212
212
|
private readonly leafCacheDirectoriesLoaded = new Set<string>();
|
|
213
|
+
private readonly configuredPrivateRepositories = new Set<string>();
|
|
214
|
+
private readonly leafCacheDirtyDirectories = new Set<string>();
|
|
213
215
|
private blobCacheBytes = 0;
|
|
214
216
|
|
|
215
217
|
constructor(options: SnapshotStoreOptions = {}) {
|
|
@@ -741,6 +743,61 @@ export class SnapshotStore {
|
|
|
741
743
|
return (await this.readBlobOperation(id, [{ rootPath, blobId, relativePath }], false))[0]!;
|
|
742
744
|
}
|
|
743
745
|
|
|
746
|
+
/** 按 root 批量预取普通文件 blob;membership 与 manifest 校验仍走只读路径。 */
|
|
747
|
+
async prefetchBlobs(id: ManifestId, requests: readonly SnapshotBlobRequest[]): Promise<void> {
|
|
748
|
+
if (requests.length === 0) return;
|
|
749
|
+
for (const request of requests) {
|
|
750
|
+
relativeSafePath("/", request.rootPath);
|
|
751
|
+
if (!isObjectId(request.blobId)) {
|
|
752
|
+
throw new SnapshotStoreError("object_missing", "blob ID 无效");
|
|
753
|
+
}
|
|
754
|
+
if (request.relativePath === undefined) {
|
|
755
|
+
throw new SnapshotStoreError("object_missing", "blob 预取必须提供 root-relative path");
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
const manifestPath = await this.findManifestPath(id);
|
|
759
|
+
const manifest = await this.loadManifest(id);
|
|
760
|
+
const roots = new Map(manifest.roots.map((root) => [root.relativeRoot, root]));
|
|
761
|
+
const storeDirectory = dirname(dirname(manifestPath));
|
|
762
|
+
const byRoot = new Map<string, SnapshotBlobRequest[]>();
|
|
763
|
+
for (const request of requests) {
|
|
764
|
+
const grouped = byRoot.get(request.rootPath) ?? [];
|
|
765
|
+
grouped.push(request);
|
|
766
|
+
byRoot.set(request.rootPath, grouped);
|
|
767
|
+
}
|
|
768
|
+
try {
|
|
769
|
+
const grouped = new Map<string, Map<string, CapturedTreeEntry>>();
|
|
770
|
+
for (const [rootPath, rootRequests] of byRoot) {
|
|
771
|
+
const root = roots.get(rootPath);
|
|
772
|
+
if (root === undefined) {
|
|
773
|
+
throw new SnapshotStoreError("root_not_found", "manifest 中不存在指定 root");
|
|
774
|
+
}
|
|
775
|
+
if (root.state !== "active" || root.treeId === null) {
|
|
776
|
+
throw new SnapshotStoreError("object_missing", "指定 root 没有可读取的 tree");
|
|
777
|
+
}
|
|
778
|
+
const gitDirectory = this.rootGitDirectory(storeDirectory, root);
|
|
779
|
+
const entries = await this.readTreeEntries(gitDirectory, root.treeId);
|
|
780
|
+
const byPath = new Map(entries.map((entry) => [entry.relativePath, entry]));
|
|
781
|
+
const unique = grouped.get(gitDirectory) ?? new Map<string, CapturedTreeEntry>();
|
|
782
|
+
for (const request of rootRequests) {
|
|
783
|
+
const safeRelativePath = relativeSafePath("/", request.relativePath!);
|
|
784
|
+
const entry = byPath.get(safeRelativePath);
|
|
785
|
+
if (entry === undefined || entry.objectId !== request.blobId) {
|
|
786
|
+
throw new SnapshotStoreError("object_missing", "blob 不属于指定 root tree path");
|
|
787
|
+
}
|
|
788
|
+
unique.set(entry.objectId, entry);
|
|
789
|
+
}
|
|
790
|
+
grouped.set(gitDirectory, unique);
|
|
791
|
+
}
|
|
792
|
+
for (const [gitDirectory, unique] of grouped) {
|
|
793
|
+
await this.preloadBlobBytes(gitDirectory, [...unique.values()]);
|
|
794
|
+
}
|
|
795
|
+
} catch (error) {
|
|
796
|
+
if (error instanceof SnapshotStoreError) throw error;
|
|
797
|
+
throw new SnapshotStoreError("object_missing", "blob 无法预取", { cause: error });
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
744
801
|
private readBlobsValidated(
|
|
745
802
|
id: ManifestId,
|
|
746
803
|
requests: readonly SnapshotBlobRequest[],
|
|
@@ -1155,9 +1212,10 @@ export class SnapshotStore {
|
|
|
1155
1212
|
|
|
1156
1213
|
private rememberVisibleLeaves(update: VisibleLeafCacheUpdate): void {
|
|
1157
1214
|
const { gitDirectory, staged, inclusions } = update;
|
|
1215
|
+
const previous = this.visibleLeafCache.get(gitDirectory);
|
|
1158
1216
|
const cache = inclusions !== null && inclusions.length === 0
|
|
1159
1217
|
? new Map<string, CachedVisibleLeaf>()
|
|
1160
|
-
: new Map(
|
|
1218
|
+
: new Map(previous);
|
|
1161
1219
|
if (inclusions !== null && inclusions.length > 0) {
|
|
1162
1220
|
for (const relativePath of cache.keys()) {
|
|
1163
1221
|
if (inclusions.some((inclusion) => isPathAtOrBelow(inclusion, relativePath))) {
|
|
@@ -1174,6 +1232,9 @@ export class SnapshotStore {
|
|
|
1174
1232
|
}
|
|
1175
1233
|
cache.set(leaf.relativePath, { ...leaf, objectId, verifiedAtNs: staged.verifiedAtNs });
|
|
1176
1234
|
}
|
|
1235
|
+
if (!samePersistedLeafCache(previous, cache)) {
|
|
1236
|
+
this.leafCacheDirtyDirectories.add(storeDirectoryForGitDirectory(gitDirectory));
|
|
1237
|
+
}
|
|
1177
1238
|
this.visibleLeafCache.set(gitDirectory, cache);
|
|
1178
1239
|
}
|
|
1179
1240
|
|
|
@@ -1210,6 +1271,7 @@ export class SnapshotStore {
|
|
|
1210
1271
|
|
|
1211
1272
|
/** 把当前 storeDirectory 范围内的叶子缓存原子写入磁盘(best-effort)。 */
|
|
1212
1273
|
private async persistLeafCache(storeDirectory: string): Promise<void> {
|
|
1274
|
+
if (!this.leafCacheDirtyDirectories.has(storeDirectory)) return;
|
|
1213
1275
|
const prefix = `${storeDirectory}${sep}`;
|
|
1214
1276
|
const entries: Record<string, Record<string, PersistedLeafCacheEntry>> = {};
|
|
1215
1277
|
for (const [gitDirectory, cache] of this.visibleLeafCache) {
|
|
@@ -1235,6 +1297,7 @@ export class SnapshotStore {
|
|
|
1235
1297
|
Buffer.from(JSON.stringify({ schemaVersion: 1, entries }), "utf8"),
|
|
1236
1298
|
0o600,
|
|
1237
1299
|
);
|
|
1300
|
+
this.leafCacheDirtyDirectories.delete(storeDirectory);
|
|
1238
1301
|
} catch {
|
|
1239
1302
|
// 缓存写入是 best-effort:失败只影响下次性能,不影响正确性。
|
|
1240
1303
|
}
|
|
@@ -1428,9 +1491,11 @@ export class SnapshotStore {
|
|
|
1428
1491
|
}
|
|
1429
1492
|
|
|
1430
1493
|
private async configurePrivateRepository(gitDirectory: string): Promise<void> {
|
|
1494
|
+
if (this.configuredPrivateRepositories.has(gitDirectory)) return;
|
|
1431
1495
|
const environment = cleanGitEnvironment();
|
|
1432
1496
|
await this.runGit(["--git-dir", gitDirectory, "config", "gc.auto", "0"], { env: environment });
|
|
1433
1497
|
await this.runGit(["--git-dir", gitDirectory, "config", "maintenance.auto", "false"], { env: environment });
|
|
1498
|
+
this.configuredPrivateRepositories.add(gitDirectory);
|
|
1434
1499
|
}
|
|
1435
1500
|
|
|
1436
1501
|
private async assertNoAlternates(gitDirectory: string): Promise<void> {
|
|
@@ -1982,6 +2047,35 @@ function blobCacheKey(gitDirectory: string, objectId: string): string {
|
|
|
1982
2047
|
return `${gitDirectory}\0${objectId}`;
|
|
1983
2048
|
}
|
|
1984
2049
|
|
|
2050
|
+
function storeDirectoryForGitDirectory(gitDirectory: string): string {
|
|
2051
|
+
return dirname(dirname(dirname(gitDirectory)));
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
function samePersistedLeafCache(
|
|
2055
|
+
left: ReadonlyMap<string, CachedVisibleLeaf> | undefined,
|
|
2056
|
+
right: ReadonlyMap<string, CachedVisibleLeaf>,
|
|
2057
|
+
): boolean {
|
|
2058
|
+
if (left === undefined) return right.size === 0;
|
|
2059
|
+
if (left.size !== right.size) return false;
|
|
2060
|
+
for (const [path, entry] of right) {
|
|
2061
|
+
const existing = left.get(path);
|
|
2062
|
+
const existingTrusted = existing !== undefined &&
|
|
2063
|
+
existing.verifiedAtNs > existing.changedAtNs + RACY_CLEAN_WINDOW_NS;
|
|
2064
|
+
const entryTrusted = entry.verifiedAtNs > entry.changedAtNs + RACY_CLEAN_WINDOW_NS;
|
|
2065
|
+
if (
|
|
2066
|
+
existing === undefined ||
|
|
2067
|
+
existing.kind !== entry.kind ||
|
|
2068
|
+
existing.mode !== entry.mode ||
|
|
2069
|
+
existing.fingerprint !== entry.fingerprint ||
|
|
2070
|
+
existing.cacheable !== entry.cacheable ||
|
|
2071
|
+
existing.objectId !== entry.objectId ||
|
|
2072
|
+
existing.changedAtNs !== entry.changedAtNs ||
|
|
2073
|
+
existingTrusted !== entryTrusted
|
|
2074
|
+
) return false;
|
|
2075
|
+
}
|
|
2076
|
+
return true;
|
|
2077
|
+
}
|
|
2078
|
+
|
|
1985
2079
|
function blobReadBatches(entries: readonly CapturedTreeEntry[]): CapturedTreeEntry[][] {
|
|
1986
2080
|
const result: CapturedTreeEntry[][] = [];
|
|
1987
2081
|
let batch: CapturedTreeEntry[] = [];
|