@davideasden/pi-undo 0.2.3 → 0.2.4
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 +1 -1
- package/src/mutation-journal.ts +6 -0
- package/src/path-safety.ts +31 -0
- package/src/pi-runtime.ts +1 -1
- package/src/quarantine.ts +1 -1
- package/src/restore-engine.ts +58 -11
- package/src/snapshot-store.ts +103 -41
package/package.json
CHANGED
package/src/mutation-journal.ts
CHANGED
|
@@ -64,6 +64,12 @@ export class MutationJournal {
|
|
|
64
64
|
return (await this.readRecords()).latest;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
async loadOrdinal(ordinal: number): Promise<MutationRecord | undefined> {
|
|
68
|
+
if (!Number.isSafeInteger(ordinal) || ordinal < 1) return undefined;
|
|
69
|
+
const record = (await this.readRecords()).latest[ordinal - 1];
|
|
70
|
+
return record?.ordinal === ordinal ? record : undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
67
73
|
begin(intent: MutationIntent): Promise<MutationRecord> {
|
|
68
74
|
return this.enqueueMutation(async () => (await this.beginManyMutation([intent]))[0]!);
|
|
69
75
|
}
|
package/src/path-safety.ts
CHANGED
|
@@ -64,6 +64,23 @@ export async function assertNoSymlinkEscape(root: string, relativePath: string):
|
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
export function pathSetsOverlap(leftPaths: readonly string[], rightPaths: readonly string[]): boolean {
|
|
68
|
+
for (const path of leftPaths) assertRelativeCandidate(path);
|
|
69
|
+
for (const path of rightPaths) assertRelativeCandidate(path);
|
|
70
|
+
const rightSet = new Set(rightPaths);
|
|
71
|
+
const sortedRight = [...rightSet].sort(comparePaths);
|
|
72
|
+
for (const path of leftPaths) {
|
|
73
|
+
if (rightSet.has(path)) return true;
|
|
74
|
+
for (let separator = path.lastIndexOf("/"); separator >= 0; separator = path.lastIndexOf("/", separator - 1)) {
|
|
75
|
+
if (rightSet.has(path.slice(0, separator))) return true;
|
|
76
|
+
}
|
|
77
|
+
const descendantPrefix = `${path}/`;
|
|
78
|
+
const candidate = sortedRight[lowerBound(sortedRight, descendantPrefix)];
|
|
79
|
+
if (candidate?.startsWith(descendantPrefix)) return true;
|
|
80
|
+
}
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
|
|
67
84
|
export function sortDeletePaths(paths: readonly string[]): string[] {
|
|
68
85
|
return sortPaths(paths, -1);
|
|
69
86
|
}
|
|
@@ -108,6 +125,20 @@ function pathDepth(path: string): number {
|
|
|
108
125
|
return path === "." ? 0 : path.split("/").length;
|
|
109
126
|
}
|
|
110
127
|
|
|
128
|
+
function lowerBound(paths: readonly string[], target: string): number {
|
|
129
|
+
let low = 0;
|
|
130
|
+
let high = paths.length;
|
|
131
|
+
while (low < high) {
|
|
132
|
+
const middle = low + Math.floor((high - low) / 2);
|
|
133
|
+
if (comparePaths(paths[middle]!, target) < 0) {
|
|
134
|
+
low = middle + 1;
|
|
135
|
+
} else {
|
|
136
|
+
high = middle;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return low;
|
|
140
|
+
}
|
|
141
|
+
|
|
111
142
|
function comparePaths(left: string, right: string): number {
|
|
112
143
|
return left < right ? -1 : left > right ? 1 : 0;
|
|
113
144
|
}
|
package/src/pi-runtime.ts
CHANGED
|
@@ -99,7 +99,7 @@ export async function createPiUndoRuntime(context: ExtensionContext, pi: Extensi
|
|
|
99
99
|
continue;
|
|
100
100
|
}
|
|
101
101
|
await quarantine.rollForwardMutation(record);
|
|
102
|
-
const latest =
|
|
102
|
+
const latest = await mutationJournal.loadOrdinal(record.ordinal);
|
|
103
103
|
if (latest === undefined) throw new Error("mutation ordinal 在恢复期间丢失");
|
|
104
104
|
await quarantine.cleanupMutation(latest);
|
|
105
105
|
}
|
package/src/quarantine.ts
CHANGED
|
@@ -771,7 +771,7 @@ export class QuarantineManager {
|
|
|
771
771
|
|
|
772
772
|
private async assertOwnedRecord(record: MutationRecord): Promise<MutationRecord> {
|
|
773
773
|
await this.assertWorkspaceIdentity();
|
|
774
|
-
const owned =
|
|
774
|
+
const owned = await this.journal.loadOrdinal(record.ordinal);
|
|
775
775
|
if (owned === undefined || immutableMutation(owned) !== immutableMutation(record)) {
|
|
776
776
|
throw new QuarantineError("unsafe_artifact", "mutation record 未被当前 journal 精确登记");
|
|
777
777
|
}
|
package/src/restore-engine.ts
CHANGED
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
type DeleteLeafRequest,
|
|
33
33
|
type ReplaceFileRequest,
|
|
34
34
|
} from "./quarantine.ts";
|
|
35
|
-
import {
|
|
35
|
+
import { SnapshotStore, SnapshotStoreError } from "./snapshot-store.ts";
|
|
36
36
|
|
|
37
37
|
const PREPARED_PLAN_CACHE_LIMIT = 16;
|
|
38
38
|
const RESTORE_FILE_BATCH_MAX_ENTRIES = 1_024;
|
|
@@ -730,14 +730,25 @@ export class RestoreEngine {
|
|
|
730
730
|
opId: string,
|
|
731
731
|
): Promise<DurablePackEntryInput[]> {
|
|
732
732
|
const paths = [...new Set([...plan.deletePaths, ...plan.writePaths])].sort(comparePaths);
|
|
733
|
+
const useValidatedBatch = SnapshotStore.supportsValidatedBlobBatch(this.store);
|
|
734
|
+
const [currentBlobBytes, targetBlobBytes] = useValidatedBatch
|
|
735
|
+
? await Promise.all([
|
|
736
|
+
this.readDurableBlobBytes(current.manifestId, paths, currentPaths),
|
|
737
|
+
this.readDurableBlobBytes(target.manifestId, paths, targetPaths),
|
|
738
|
+
])
|
|
739
|
+
: [new Map<string, Uint8Array>(), new Map<string, Uint8Array>()];
|
|
733
740
|
const result: DurablePackEntryInput[] = [];
|
|
734
741
|
for (const path of paths) {
|
|
735
742
|
const variants = new Map<string, DurableLeafInput>();
|
|
736
743
|
const absent: DurableLeafInput = { kind: "absent", fingerprint: fingerprintAbsent(path) };
|
|
737
744
|
variants.set(absent.fingerprint, absent);
|
|
738
|
-
const currentLeaf =
|
|
745
|
+
const currentLeaf = useValidatedBatch
|
|
746
|
+
? this.durableLeaf(currentPaths.get(path), currentBlobBytes.get(path))
|
|
747
|
+
: await this.durableLeafCompatible(current.manifestId, currentPaths.get(path));
|
|
739
748
|
if (currentLeaf !== undefined) variants.set(currentLeaf.fingerprint, currentLeaf);
|
|
740
|
-
const targetLeaf =
|
|
749
|
+
const targetLeaf = useValidatedBatch
|
|
750
|
+
? this.durableLeaf(targetPaths.get(path), targetBlobBytes.get(path))
|
|
751
|
+
: await this.durableLeafCompatible(target.manifestId, targetPaths.get(path));
|
|
741
752
|
if (targetLeaf !== undefined) variants.set(targetLeaf.fingerprint, targetLeaf);
|
|
742
753
|
if (currentLeaf === undefined && targetLeaf === undefined) continue;
|
|
743
754
|
const artifactId = checksum(canonicalJson({ opId, path })).slice(0, 32);
|
|
@@ -757,10 +768,50 @@ export class RestoreEngine {
|
|
|
757
768
|
return result;
|
|
758
769
|
}
|
|
759
770
|
|
|
760
|
-
private async
|
|
771
|
+
private async readDurableBlobBytes(
|
|
772
|
+
manifestId: ManifestId,
|
|
773
|
+
paths: readonly string[],
|
|
774
|
+
ownedPaths: ReadonlyMap<string, OwnedPath>,
|
|
775
|
+
): Promise<ReadonlyMap<string, Uint8Array>> {
|
|
776
|
+
const files = paths.flatMap((path) => {
|
|
777
|
+
const owned = ownedPaths.get(path);
|
|
778
|
+
if (owned?.entry.kind !== "file") return [];
|
|
779
|
+
if (owned.entry.blobId === null) {
|
|
780
|
+
throw new Error(`durable pack 普通文件缺少 blob:${owned.absolutePath}`);
|
|
781
|
+
}
|
|
782
|
+
return [{ path, owned, blobId: owned.entry.blobId }];
|
|
783
|
+
});
|
|
784
|
+
if (files.length === 0) return new Map();
|
|
785
|
+
const requests = files.map(({ owned, blobId }) => ({
|
|
786
|
+
rootPath: owned.root.relativeRoot,
|
|
787
|
+
blobId,
|
|
788
|
+
relativePath: owned.entry.relativePath,
|
|
789
|
+
}));
|
|
790
|
+
const blobs = await SnapshotStore.readBlobs(this.store, manifestId, requests);
|
|
791
|
+
if (blobs.length !== files.length) throw new Error("durable pack blob batch 数量不匹配");
|
|
792
|
+
return new Map(files.map(({ path }, index) => [path, blobs[index]!]));
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
private async durableLeafCompatible(
|
|
761
796
|
manifestId: ManifestId,
|
|
762
797
|
owned: OwnedPath | undefined,
|
|
763
798
|
): Promise<DurableLeafInput | undefined> {
|
|
799
|
+
if (owned?.entry.kind !== "file") return this.durableLeaf(owned, undefined);
|
|
800
|
+
if (owned.entry.blobId === null) {
|
|
801
|
+
throw new Error(`durable pack 普通文件缺少 blob:${owned.absolutePath}`);
|
|
802
|
+
}
|
|
803
|
+
return this.durableLeaf(owned, await this.store.readBlob(
|
|
804
|
+
manifestId,
|
|
805
|
+
owned.root.relativeRoot,
|
|
806
|
+
owned.entry.blobId,
|
|
807
|
+
owned.entry.relativePath,
|
|
808
|
+
));
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
private durableLeaf(
|
|
812
|
+
owned: OwnedPath | undefined,
|
|
813
|
+
bytes: Uint8Array | undefined,
|
|
814
|
+
): DurableLeafInput | undefined {
|
|
764
815
|
if (owned === undefined || owned.entry.kind === "directory") return undefined;
|
|
765
816
|
if (owned.entry.kind === "symlink") {
|
|
766
817
|
return {
|
|
@@ -769,13 +820,9 @@ export class RestoreEngine {
|
|
|
769
820
|
linkText: owned.entry.linkText!,
|
|
770
821
|
};
|
|
771
822
|
}
|
|
772
|
-
if (owned.entry.blobId === null
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
owned.root.relativeRoot,
|
|
776
|
-
owned.entry.blobId,
|
|
777
|
-
owned.entry.relativePath,
|
|
778
|
-
);
|
|
823
|
+
if (owned.entry.blobId === null || bytes === undefined) {
|
|
824
|
+
throw new Error(`durable pack 普通文件缺少 blob:${owned.absolutePath}`);
|
|
825
|
+
}
|
|
779
826
|
const mode = owned.entry.mode & 0o777;
|
|
780
827
|
return {
|
|
781
828
|
kind: "file",
|
package/src/snapshot-store.ts
CHANGED
|
@@ -20,7 +20,7 @@ import type {
|
|
|
20
20
|
SnapshotManifest,
|
|
21
21
|
SnapshotRoot,
|
|
22
22
|
} from "./model.ts";
|
|
23
|
-
import { assertNoSymlinkEscape, relativeSafePath } from "./path-safety.ts";
|
|
23
|
+
import { assertNoSymlinkEscape, pathSetsOverlap, relativeSafePath } from "./path-safety.ts";
|
|
24
24
|
import { RootDiscovery, type RootTopology } from "./root-discovery.ts";
|
|
25
25
|
import { WorkspaceLock } from "./workspace-lock.ts";
|
|
26
26
|
|
|
@@ -96,6 +96,12 @@ export interface CaptureOptions {
|
|
|
96
96
|
readonly excludePaths?: readonly string[];
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
export interface SnapshotBlobRequest {
|
|
100
|
+
readonly rootPath: string;
|
|
101
|
+
readonly blobId: string;
|
|
102
|
+
readonly relativePath?: string;
|
|
103
|
+
}
|
|
104
|
+
|
|
99
105
|
export type SnapshotStoreErrorCode =
|
|
100
106
|
| "capture_failed"
|
|
101
107
|
| "invalid_manifest_id"
|
|
@@ -151,6 +157,33 @@ export class SnapshotStore {
|
|
|
151
157
|
this.clock = options.clock ?? Date.now;
|
|
152
158
|
}
|
|
153
159
|
|
|
160
|
+
static supportsValidatedBlobBatch(store: SnapshotStore): boolean {
|
|
161
|
+
return store instanceof SnapshotStore &&
|
|
162
|
+
store.readBlob === SnapshotStore.prototype.readBlob &&
|
|
163
|
+
store.loadManifest === SnapshotStore.prototype.loadManifest;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
static async readBlobs(
|
|
167
|
+
store: SnapshotStore,
|
|
168
|
+
id: ManifestId,
|
|
169
|
+
requests: readonly SnapshotBlobRequest[],
|
|
170
|
+
): Promise<readonly Uint8Array[]> {
|
|
171
|
+
if (requests.length === 0) return [];
|
|
172
|
+
if (SnapshotStore.supportsValidatedBlobBatch(store)) {
|
|
173
|
+
return (store as SnapshotStore).readBlobsValidated(id, requests);
|
|
174
|
+
}
|
|
175
|
+
const result: Uint8Array[] = [];
|
|
176
|
+
for (const request of requests) {
|
|
177
|
+
result.push(await store.readBlob(
|
|
178
|
+
id,
|
|
179
|
+
request.rootPath,
|
|
180
|
+
request.blobId,
|
|
181
|
+
request.relativePath,
|
|
182
|
+
));
|
|
183
|
+
}
|
|
184
|
+
return result;
|
|
185
|
+
}
|
|
186
|
+
|
|
154
187
|
async durableCacheDirectory(): Promise<string> {
|
|
155
188
|
const directory = join(this.storeRoot, "durable-cache");
|
|
156
189
|
await mkdir(directory, { recursive: true });
|
|
@@ -309,7 +342,10 @@ export class SnapshotStore {
|
|
|
309
342
|
}
|
|
310
343
|
|
|
311
344
|
async loadManifest(id: ManifestId): Promise<SnapshotManifest> {
|
|
312
|
-
|
|
345
|
+
return this.loadManifestFromPath(id, await this.findManifestPath(id));
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
private async loadManifestFromPath(id: ManifestId, manifestPath: string): Promise<SnapshotManifest> {
|
|
313
349
|
try {
|
|
314
350
|
const value: unknown = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
315
351
|
const manifest = assertManifest(value);
|
|
@@ -349,10 +385,10 @@ export class SnapshotStore {
|
|
|
349
385
|
const gitDirectory = this.rootGitDirectory(storeDirectory, root);
|
|
350
386
|
await this.assertNoAlternates(gitDirectory);
|
|
351
387
|
const entries = await this.readTreeEntries(gitDirectory, root.treeId, rootScope);
|
|
352
|
-
if (
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
))
|
|
388
|
+
if (pathSetsOverlap(
|
|
389
|
+
root.ignoredPresentPaths,
|
|
390
|
+
entries.map((entry) => entry.relativePath),
|
|
391
|
+
)) {
|
|
356
392
|
throw new SnapshotStoreError("object_missing", "ignored-present proof 与 root tree 冲突");
|
|
357
393
|
}
|
|
358
394
|
await this.assertObjectsComplete(gitDirectory, root.treeId, entries);
|
|
@@ -432,41 +468,67 @@ export class SnapshotStore {
|
|
|
432
468
|
blobId: string,
|
|
433
469
|
relativePath?: string,
|
|
434
470
|
): Promise<Uint8Array> {
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
471
|
+
return (await this.readBlobOperation(id, [{ rootPath, blobId, relativePath }], false))[0]!;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
private readBlobsValidated(
|
|
475
|
+
id: ManifestId,
|
|
476
|
+
requests: readonly SnapshotBlobRequest[],
|
|
477
|
+
): Promise<readonly Uint8Array[]> {
|
|
478
|
+
return this.readBlobOperation(id, requests, true);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
private async readBlobOperation(
|
|
482
|
+
id: ManifestId,
|
|
483
|
+
requests: readonly SnapshotBlobRequest[],
|
|
484
|
+
revalidateManifest: boolean,
|
|
485
|
+
): Promise<readonly Uint8Array[]> {
|
|
486
|
+
for (const request of requests) {
|
|
487
|
+
relativeSafePath("/", request.rootPath);
|
|
488
|
+
if (!isObjectId(request.blobId)) {
|
|
489
|
+
throw new SnapshotStoreError("object_missing", "blob ID 无效");
|
|
490
|
+
}
|
|
438
491
|
}
|
|
439
492
|
const manifestPath = await this.findManifestPath(id);
|
|
440
|
-
const manifest =
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
}
|
|
445
|
-
if (root.state !== "active" || root.treeId === null) {
|
|
446
|
-
throw new SnapshotStoreError("object_missing", "指定 root 没有可读取的 tree");
|
|
447
|
-
}
|
|
448
|
-
|
|
493
|
+
const manifest = revalidateManifest
|
|
494
|
+
? await this.loadManifestFromPath(id, manifestPath)
|
|
495
|
+
: await this.loadManifest(id);
|
|
496
|
+
const roots = new Map(manifest.roots.map((root) => [root.relativeRoot, root]));
|
|
449
497
|
const storeDirectory = dirname(dirname(manifestPath));
|
|
450
|
-
const gitDirectory = this.rootGitDirectory(storeDirectory, root);
|
|
451
498
|
try {
|
|
452
|
-
const
|
|
453
|
-
|
|
454
|
-
const
|
|
455
|
-
if (
|
|
456
|
-
throw new SnapshotStoreError("
|
|
499
|
+
const result: Uint8Array[] = [];
|
|
500
|
+
for (const request of requests) {
|
|
501
|
+
const root = roots.get(request.rootPath);
|
|
502
|
+
if (root === undefined) {
|
|
503
|
+
throw new SnapshotStoreError("root_not_found", "manifest 中不存在指定 root");
|
|
457
504
|
}
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
let ownedBlobId = this.treeBlobMembership.get(membershipKey);
|
|
461
|
-
if (ownedBlobId === undefined) {
|
|
462
|
-
await this.readTreeEntries(gitDirectory, root.treeId, [safeRelativePath]);
|
|
463
|
-
ownedBlobId = this.treeBlobMembership.get(membershipKey);
|
|
505
|
+
if (root.state !== "active" || root.treeId === null) {
|
|
506
|
+
throw new SnapshotStoreError("object_missing", "指定 root 没有可读取的 tree");
|
|
464
507
|
}
|
|
465
|
-
|
|
466
|
-
|
|
508
|
+
const gitDirectory = this.rootGitDirectory(storeDirectory, root);
|
|
509
|
+
const safeRelativePath = request.relativePath === undefined
|
|
510
|
+
? undefined
|
|
511
|
+
: relativeSafePath("/", request.relativePath);
|
|
512
|
+
if (safeRelativePath === undefined) {
|
|
513
|
+
const entries = await this.readTreeEntries(gitDirectory, root.treeId);
|
|
514
|
+
if (!entries.some((entry) => entry.objectId === request.blobId)) {
|
|
515
|
+
throw new SnapshotStoreError("object_missing", "blob 不属于指定 root tree");
|
|
516
|
+
}
|
|
517
|
+
} else {
|
|
518
|
+
const membershipKey = treeBlobMembershipKey(gitDirectory, root.treeId, safeRelativePath);
|
|
519
|
+
let ownedBlobId = this.treeBlobMembership.get(membershipKey);
|
|
520
|
+
if (ownedBlobId === undefined) {
|
|
521
|
+
await this.readTreeEntries(gitDirectory, root.treeId, [safeRelativePath]);
|
|
522
|
+
ownedBlobId = this.treeBlobMembership.get(membershipKey);
|
|
523
|
+
}
|
|
524
|
+
if (ownedBlobId !== request.blobId) {
|
|
525
|
+
throw new SnapshotStoreError("object_missing", "blob 不属于指定 root tree path");
|
|
526
|
+
}
|
|
467
527
|
}
|
|
528
|
+
result.push(new Uint8Array(await this.readBlobBytes(gitDirectory, request.blobId)));
|
|
468
529
|
}
|
|
469
|
-
|
|
530
|
+
if (revalidateManifest) await this.loadManifestFromPath(id, manifestPath);
|
|
531
|
+
return result;
|
|
470
532
|
} catch (error) {
|
|
471
533
|
if (error instanceof SnapshotStoreError) {
|
|
472
534
|
throw error;
|
|
@@ -634,7 +696,7 @@ export class SnapshotStore {
|
|
|
634
696
|
gitBacked: boolean,
|
|
635
697
|
inclusions: readonly string[] | null,
|
|
636
698
|
exclusions: readonly string[],
|
|
637
|
-
exactExclusions:
|
|
699
|
+
exactExclusions: ReadonlySet<string>,
|
|
638
700
|
): Promise<string[]> {
|
|
639
701
|
if (inclusions === null) {
|
|
640
702
|
return [];
|
|
@@ -657,7 +719,7 @@ export class SnapshotStore {
|
|
|
657
719
|
for (const relativePath of parseNulPaths(output)) {
|
|
658
720
|
if (
|
|
659
721
|
exclusions.some((excluded) => isPathAtOrBelow(excluded, relativePath)) ||
|
|
660
|
-
exactExclusions.
|
|
722
|
+
exactExclusions.has(relativePath)
|
|
661
723
|
) {
|
|
662
724
|
continue;
|
|
663
725
|
}
|
|
@@ -686,7 +748,7 @@ export class SnapshotStore {
|
|
|
686
748
|
gitBacked: boolean,
|
|
687
749
|
inclusions: readonly string[] | null,
|
|
688
750
|
exclusions: readonly string[],
|
|
689
|
-
exactExclusions:
|
|
751
|
+
exactExclusions: ReadonlySet<string>,
|
|
690
752
|
): Promise<void> {
|
|
691
753
|
const leaves = await this.collectVisibleLeaves(
|
|
692
754
|
cwd,
|
|
@@ -758,7 +820,7 @@ export class SnapshotStore {
|
|
|
758
820
|
gitBacked: boolean,
|
|
759
821
|
inclusions: readonly string[] | null,
|
|
760
822
|
exclusions: readonly string[],
|
|
761
|
-
exactExclusions:
|
|
823
|
+
exactExclusions: ReadonlySet<string>,
|
|
762
824
|
excludeDeleted = false,
|
|
763
825
|
): Promise<string[]> {
|
|
764
826
|
if (inclusions === null) return [];
|
|
@@ -792,7 +854,7 @@ export class SnapshotStore {
|
|
|
792
854
|
return parseNulPaths(output).filter((relativePath) =>
|
|
793
855
|
!deletedPaths.has(relativePath) &&
|
|
794
856
|
!exclusions.some((excluded) => isPathAtOrBelow(excluded, relativePath)) &&
|
|
795
|
-
!exactExclusions.
|
|
857
|
+
!exactExclusions.has(relativePath));
|
|
796
858
|
}
|
|
797
859
|
|
|
798
860
|
private async collectVisibleLeaves(
|
|
@@ -801,7 +863,7 @@ export class SnapshotStore {
|
|
|
801
863
|
gitBacked: boolean,
|
|
802
864
|
inclusions: readonly string[] | null,
|
|
803
865
|
exclusions: readonly string[],
|
|
804
|
-
exactExclusions:
|
|
866
|
+
exactExclusions: ReadonlySet<string>,
|
|
805
867
|
): Promise<VisibleLeaf[]> {
|
|
806
868
|
const paths = await this.queryVisibleLeafPaths(
|
|
807
869
|
cwd,
|
|
@@ -1196,7 +1258,7 @@ function ownedArtifactExclusions(
|
|
|
1196
1258
|
roots: readonly DiscoveryRoot[],
|
|
1197
1259
|
rootPath: string,
|
|
1198
1260
|
exclusions: readonly string[],
|
|
1199
|
-
): string
|
|
1261
|
+
): ReadonlySet<string> {
|
|
1200
1262
|
const result: string[] = [];
|
|
1201
1263
|
for (const exclusion of exclusions) {
|
|
1202
1264
|
const owner = roots
|
|
@@ -1211,7 +1273,7 @@ function ownedArtifactExclusions(
|
|
|
1211
1273
|
}
|
|
1212
1274
|
result.push(rootRelativePath(rootPath, exclusion));
|
|
1213
1275
|
}
|
|
1214
|
-
return result;
|
|
1276
|
+
return new Set(result);
|
|
1215
1277
|
}
|
|
1216
1278
|
|
|
1217
1279
|
function rootRelativeScope(rootPath: string, scope: readonly string[] | undefined): string[] | undefined {
|