nano-git 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/dist/workdir/change-index-plan.mjs +76 -0
- package/dist/workdir/change-index.d.mts +21 -0
- package/dist/workdir/change-index.mjs +413 -0
- package/dist/workdir/core.d.mts +64 -63
- package/dist/workdir/directory-view.mjs +197 -0
- package/dist/workdir/dirty-dir-plan.mjs +73 -0
- package/dist/workdir/dirty-dir.d.mts +28 -0
- package/dist/workdir/dirty-dir.mjs +32 -0
- package/dist/workdir/file-backend.d.mts +34 -12
- package/dist/workdir/file-backend.mjs +161 -94
- package/dist/workdir/file.d.mts +2 -2
- package/dist/workdir/file.mjs +2 -2
- package/dist/workdir/ids.d.mts +1 -1
- package/dist/workdir/ids.mjs +3 -3
- package/dist/workdir/memory-backend.mjs +34 -50
- package/dist/workdir/memory.d.mts +2 -3
- package/dist/workdir/memory.mjs +2 -3
- package/dist/workdir/nodes.d.mts +7 -7
- package/dist/workdir/nodes.mjs +3 -9
- package/dist/workdir/overlay.d.mts +3 -3
- package/dist/workdir/sqlite-backend.d.mts +31 -14
- package/dist/workdir/sqlite-backend.mjs +238 -142
- package/dist/workdir/sqlite.d.mts +2 -2
- package/dist/workdir/sqlite.mjs +2 -2
- package/dist/workdir/state-store.d.mts +23 -12
- package/dist/workdir/workdir-path.mjs +348 -0
- package/dist/workdir/workdir-transaction.mjs +115 -0
- package/dist/workdir/workdir.d.mts +30 -0
- package/dist/workdir/workdir.mjs +280 -0
- package/dist/workdir/write-tree.mjs +108 -44
- package/package.json +2 -1
- package/dist/workdir/change-log.d.mts +0 -25
- package/dist/workdir/change-log.mjs +0 -69
- package/dist/workdir/memory-backend.d.mts +0 -17
- package/dist/workdir/session-id.mjs +0 -18
- package/dist/workdir/session-internal.mjs +0 -141
- package/dist/workdir/session.d.mts +0 -30
- package/dist/workdir/session.mjs +0 -407
|
@@ -1,82 +1,71 @@
|
|
|
1
|
+
import { sha1 } from "../core/types.mjs";
|
|
1
2
|
import { createRootDirectoryNode } from "./nodes.mjs";
|
|
2
|
-
import {
|
|
3
|
-
import { openVirtualWorkdirSession } from "./session.mjs";
|
|
3
|
+
import { openVirtualWorkdir } from "./workdir.mjs";
|
|
4
4
|
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { dirname, join } from "node:path";
|
|
6
6
|
//#region src/workdir/file-backend.ts
|
|
7
7
|
/**
|
|
8
|
-
* Virtual Workdir
|
|
8
|
+
* Virtual Workdir 文件系统持久化实现
|
|
9
9
|
*/
|
|
10
|
-
const FILE_WORKDIR_MANIFEST_VERSION =
|
|
10
|
+
const FILE_WORKDIR_MANIFEST_VERSION = 5;
|
|
11
11
|
const FILE_WORKDIR_TRANSACTION_SNAPSHOT_SUFFIX = ".txn-snapshot";
|
|
12
12
|
/**
|
|
13
|
-
*
|
|
13
|
+
* 打开基于目录持久化的 VirtualWorkdir
|
|
14
14
|
*
|
|
15
15
|
* @example
|
|
16
16
|
* ```ts
|
|
17
|
-
* const
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
17
|
+
* const workdir = openFileVirtualWorkdir(repo.objects, "/tmp/workdir", {
|
|
18
|
+
* baseTree: tree,
|
|
19
|
+
* create: true,
|
|
20
|
+
* });
|
|
21
|
+
* expect(workdir.baseTree).toBe(tree);
|
|
21
22
|
* ```
|
|
22
23
|
*/
|
|
23
|
-
function
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (!existsSync(sessionsRoot)) return [];
|
|
47
|
-
return readdirSync(sessionsRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory()).filter((entry) => !entry.name.endsWith(FILE_WORKDIR_TRANSACTION_SNAPSHOT_SUFFIX)).filter((entry) => existsSync(getManifestPath(join(sessionsRoot, entry.name)))).map((entry) => decodePathToken(entry.name)).filter((sessionId) => {
|
|
48
|
-
try {
|
|
49
|
-
validateSessionIntegrity(sessionsRoot, sessionId);
|
|
50
|
-
return true;
|
|
51
|
-
} catch {
|
|
52
|
-
return false;
|
|
53
|
-
}
|
|
54
|
-
}).sort();
|
|
55
|
-
}
|
|
56
|
-
};
|
|
24
|
+
function openFileVirtualWorkdir(source, workdirDir, options) {
|
|
25
|
+
const store = createFileVirtualWorkdirStateStore(workdirDir);
|
|
26
|
+
if (!hasFileVirtualWorkdir(workdirDir)) {
|
|
27
|
+
if (options.create !== true) throw new Error(`Virtual workdir not found: ${workdirDir}`);
|
|
28
|
+
store.reset(options.baseTree);
|
|
29
|
+
}
|
|
30
|
+
validateFileVirtualWorkdirIntegrity(workdirDir);
|
|
31
|
+
return openVirtualWorkdir(source, store);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* 删除指定目录上的持久化 VirtualWorkdir
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```ts
|
|
38
|
+
* deleteFileVirtualWorkdir("/tmp/workdir");
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
function deleteFileVirtualWorkdir(workdirDir) {
|
|
42
|
+
if (!hasFileVirtualWorkdir(workdirDir)) throw new Error(`Virtual workdir not found: ${workdirDir}`);
|
|
43
|
+
rmSync(workdirDir, {
|
|
44
|
+
recursive: true,
|
|
45
|
+
force: true
|
|
46
|
+
});
|
|
57
47
|
}
|
|
58
48
|
/**
|
|
59
|
-
*
|
|
49
|
+
* 创建单个文件系统 VirtualWorkdir 的状态存储
|
|
60
50
|
*
|
|
61
51
|
* @example
|
|
62
52
|
* ```ts
|
|
63
|
-
* const store = createFileVirtualWorkdirStateStore("/tmp/
|
|
53
|
+
* const store = createFileVirtualWorkdirStateStore("/tmp/workdir");
|
|
64
54
|
* expect(store.kind).toBe("file");
|
|
65
55
|
* ```
|
|
66
56
|
*/
|
|
67
|
-
function createFileVirtualWorkdirStateStore(
|
|
68
|
-
const
|
|
69
|
-
const
|
|
70
|
-
const contentDir = getContentDir(sessionDir);
|
|
57
|
+
function createFileVirtualWorkdirStateStore(workdirDir) {
|
|
58
|
+
const manifestPath = getManifestPath(workdirDir);
|
|
59
|
+
const contentDir = getContentDir(workdirDir);
|
|
71
60
|
return {
|
|
72
61
|
kind: "file",
|
|
73
62
|
transact(fn) {
|
|
74
|
-
const snapshotDir = `${
|
|
63
|
+
const snapshotDir = `${workdirDir}${FILE_WORKDIR_TRANSACTION_SNAPSHOT_SUFFIX}`;
|
|
75
64
|
rmSync(snapshotDir, {
|
|
76
65
|
recursive: true,
|
|
77
66
|
force: true
|
|
78
67
|
});
|
|
79
|
-
if (existsSync(
|
|
68
|
+
if (existsSync(workdirDir)) copyDirectoryRecursive(workdirDir, snapshotDir);
|
|
80
69
|
try {
|
|
81
70
|
const result = fn();
|
|
82
71
|
rmSync(snapshotDir, {
|
|
@@ -85,11 +74,11 @@ function createFileVirtualWorkdirStateStore(sessionsRoot, sessionId) {
|
|
|
85
74
|
});
|
|
86
75
|
return result;
|
|
87
76
|
} catch (error) {
|
|
88
|
-
rmSync(
|
|
77
|
+
rmSync(workdirDir, {
|
|
89
78
|
recursive: true,
|
|
90
79
|
force: true
|
|
91
80
|
});
|
|
92
|
-
if (existsSync(snapshotDir)) renameSync(snapshotDir,
|
|
81
|
+
if (existsSync(snapshotDir)) renameSync(snapshotDir, workdirDir);
|
|
93
82
|
throw error;
|
|
94
83
|
}
|
|
95
84
|
},
|
|
@@ -97,7 +86,7 @@ function createFileVirtualWorkdirStateStore(sessionsRoot, sessionId) {
|
|
|
97
86
|
return readManifest(manifestPath).baseTree;
|
|
98
87
|
},
|
|
99
88
|
writeBaseTree(baseTree) {
|
|
100
|
-
updateManifest(
|
|
89
|
+
updateManifest(workdirDir, (manifest) => ({
|
|
101
90
|
...manifest,
|
|
102
91
|
baseTree
|
|
103
92
|
}));
|
|
@@ -108,7 +97,7 @@ function createFileVirtualWorkdirStateStore(sessionsRoot, sessionId) {
|
|
|
108
97
|
return restoreNode(record, contentDir);
|
|
109
98
|
},
|
|
110
99
|
setNode(node) {
|
|
111
|
-
updateManifest(
|
|
100
|
+
updateManifest(workdirDir, (manifest) => {
|
|
112
101
|
const record = persistNode(contentDir, node);
|
|
113
102
|
return {
|
|
114
103
|
...manifest,
|
|
@@ -120,7 +109,7 @@ function createFileVirtualWorkdirStateStore(sessionsRoot, sessionId) {
|
|
|
120
109
|
});
|
|
121
110
|
},
|
|
122
111
|
deleteNode(id) {
|
|
123
|
-
updateManifest(
|
|
112
|
+
updateManifest(workdirDir, (manifest) => {
|
|
124
113
|
if (manifest.nodes[id] === void 0) return manifest;
|
|
125
114
|
const { [id]: _deleted, ...rest } = manifest.nodes;
|
|
126
115
|
return {
|
|
@@ -129,56 +118,79 @@ function createFileVirtualWorkdirStateStore(sessionsRoot, sessionId) {
|
|
|
129
118
|
};
|
|
130
119
|
});
|
|
131
120
|
},
|
|
132
|
-
|
|
133
|
-
|
|
121
|
+
listChangeRecords() {
|
|
122
|
+
return readManifest(manifestPath).changeRecords.map(restoreChangeRecord);
|
|
123
|
+
},
|
|
124
|
+
getChangeRecord(path) {
|
|
125
|
+
return readManifest(manifestPath).changeRecords.map(restoreChangeRecord).find((record) => record.path === path) ?? null;
|
|
126
|
+
},
|
|
127
|
+
setChangeRecord(record) {
|
|
128
|
+
updateManifest(workdirDir, (manifest) => {
|
|
129
|
+
const others = manifest.changeRecords.filter((item) => item.path !== record.path);
|
|
130
|
+
return {
|
|
131
|
+
...manifest,
|
|
132
|
+
changeRecords: [...others, serializeChangeRecord(record)].sort((left, right) => left.path.localeCompare(right.path))
|
|
133
|
+
};
|
|
134
|
+
});
|
|
135
|
+
},
|
|
136
|
+
deleteChangeRecord(path) {
|
|
137
|
+
updateManifest(workdirDir, (manifest) => ({
|
|
134
138
|
...manifest,
|
|
135
|
-
|
|
139
|
+
changeRecords: manifest.changeRecords.filter((item) => item.path !== path)
|
|
136
140
|
}));
|
|
137
141
|
},
|
|
138
|
-
|
|
139
|
-
return readManifest(manifestPath).
|
|
142
|
+
listDirtyDirSummaries() {
|
|
143
|
+
return readManifest(manifestPath).dirtyDirSummaries.map(restoreDirtyDirSummary);
|
|
144
|
+
},
|
|
145
|
+
getDirtyDirSummary(path) {
|
|
146
|
+
return readManifest(manifestPath).dirtyDirSummaries.map(restoreDirtyDirSummary).find((summary) => summary.path === path) ?? null;
|
|
147
|
+
},
|
|
148
|
+
setDirtyDirSummary(summary) {
|
|
149
|
+
updateManifest(workdirDir, (manifest) => {
|
|
150
|
+
const others = manifest.dirtyDirSummaries.filter((item) => item.path !== summary.path);
|
|
151
|
+
return {
|
|
152
|
+
...manifest,
|
|
153
|
+
dirtyDirSummaries: [...others, serializeDirtyDirSummary(summary)].sort((left, right) => left.path.localeCompare(right.path))
|
|
154
|
+
};
|
|
155
|
+
});
|
|
140
156
|
},
|
|
141
|
-
|
|
142
|
-
updateManifest(
|
|
157
|
+
deleteDirtyDirSummary(path) {
|
|
158
|
+
updateManifest(workdirDir, (manifest) => ({
|
|
143
159
|
...manifest,
|
|
144
|
-
|
|
160
|
+
dirtyDirSummaries: manifest.dirtyDirSummaries.filter((item) => item.path !== path)
|
|
145
161
|
}));
|
|
146
162
|
},
|
|
147
163
|
reset(baseTree) {
|
|
148
|
-
rmSync(
|
|
164
|
+
rmSync(workdirDir, {
|
|
149
165
|
recursive: true,
|
|
150
166
|
force: true
|
|
151
167
|
});
|
|
152
|
-
|
|
168
|
+
ensureWorkdirDirs(workdirDir, contentDir);
|
|
153
169
|
writeManifestAtomic(manifestPath, createManifest(baseTree, { [createRootDirectoryNode(baseTree).id]: serializeDirectoryNode(createRootDirectoryNode(baseTree)) }));
|
|
154
170
|
}
|
|
155
171
|
};
|
|
156
172
|
}
|
|
157
|
-
function
|
|
158
|
-
return existsSync(getManifestPath(
|
|
173
|
+
function hasFileVirtualWorkdir(workdirDir) {
|
|
174
|
+
return existsSync(getManifestPath(workdirDir));
|
|
159
175
|
}
|
|
160
|
-
function
|
|
161
|
-
const
|
|
162
|
-
const manifest = readManifest(getManifestPath(sessionDir));
|
|
176
|
+
function validateFileVirtualWorkdirIntegrity(workdirDir) {
|
|
177
|
+
const manifest = readManifest(getManifestPath(workdirDir));
|
|
163
178
|
const root = manifest.nodes.root;
|
|
164
|
-
if (root === void 0) throw new Error(`Virtual workdir
|
|
165
|
-
if (restoreNode(root, getContentDir(
|
|
166
|
-
for (const record of Object.values(manifest.nodes)) restoreNode(record, getContentDir(
|
|
179
|
+
if (root === void 0) throw new Error(`Virtual workdir is corrupted: missing root node for ${workdirDir}`);
|
|
180
|
+
if (restoreNode(root, getContentDir(workdirDir)).state.kind !== "directory") throw new Error(`Virtual workdir is corrupted: root node is not a directory for ${workdirDir}`);
|
|
181
|
+
for (const record of Object.values(manifest.nodes)) restoreNode(record, getContentDir(workdirDir));
|
|
167
182
|
}
|
|
168
|
-
function
|
|
169
|
-
return join(
|
|
183
|
+
function getManifestPath(workdirDir) {
|
|
184
|
+
return join(workdirDir, "manifest.json");
|
|
170
185
|
}
|
|
171
|
-
function
|
|
172
|
-
return join(
|
|
173
|
-
}
|
|
174
|
-
function getContentDir(sessionDir) {
|
|
175
|
-
return join(sessionDir, "content");
|
|
186
|
+
function getContentDir(workdirDir) {
|
|
187
|
+
return join(workdirDir, "content");
|
|
176
188
|
}
|
|
177
189
|
function getContentPath(contentDir, payloadRef) {
|
|
178
190
|
return join(contentDir, `${encodePathToken(payloadRef)}.bin`);
|
|
179
191
|
}
|
|
180
|
-
function
|
|
181
|
-
mkdirSync(
|
|
192
|
+
function ensureWorkdirDirs(workdirDir, contentDir) {
|
|
193
|
+
mkdirSync(workdirDir, { recursive: true });
|
|
182
194
|
mkdirSync(contentDir, { recursive: true });
|
|
183
195
|
}
|
|
184
196
|
function copyDirectoryRecursive(sourceDir, targetDir) {
|
|
@@ -197,28 +209,30 @@ function createManifest(baseTree, nodes) {
|
|
|
197
209
|
return {
|
|
198
210
|
formatVersion: FILE_WORKDIR_MANIFEST_VERSION,
|
|
199
211
|
baseTree,
|
|
200
|
-
|
|
201
|
-
|
|
212
|
+
nodes,
|
|
213
|
+
changeRecords: [],
|
|
214
|
+
dirtyDirSummaries: []
|
|
202
215
|
};
|
|
203
216
|
}
|
|
204
|
-
function updateManifest(
|
|
205
|
-
const manifestPath = getManifestPath(
|
|
206
|
-
|
|
217
|
+
function updateManifest(workdirDir, updater) {
|
|
218
|
+
const manifestPath = getManifestPath(workdirDir);
|
|
219
|
+
ensureWorkdirDirs(workdirDir, getContentDir(workdirDir));
|
|
207
220
|
writeManifestAtomic(manifestPath, updater(readManifest(manifestPath)));
|
|
208
221
|
}
|
|
209
222
|
function writeManifestAtomic(path, manifest) {
|
|
210
223
|
writeJsonAtomic(path, manifest);
|
|
211
224
|
}
|
|
212
225
|
function readManifest(manifestPath) {
|
|
213
|
-
if (!existsSync(manifestPath)) throw new Error(`Virtual workdir
|
|
226
|
+
if (!existsSync(manifestPath)) throw new Error(`Virtual workdir manifest not found: ${manifestPath}`);
|
|
214
227
|
const manifest = readJson(manifestPath);
|
|
215
228
|
validateManifest(manifest);
|
|
216
229
|
return manifest;
|
|
217
230
|
}
|
|
218
231
|
function validateManifest(manifest) {
|
|
219
232
|
if (typeof manifest.baseTree !== "string" || manifest.baseTree.length === 0) throw new Error("Invalid virtual workdir manifest baseTree");
|
|
220
|
-
if (!Array.isArray(manifest.changes)) throw new Error("Invalid virtual workdir manifest changes");
|
|
221
233
|
if (typeof manifest.nodes !== "object" || manifest.nodes === null || Array.isArray(manifest.nodes)) throw new Error("Invalid virtual workdir manifest nodes");
|
|
234
|
+
if (!Array.isArray(manifest.changeRecords)) throw new Error("Invalid virtual workdir manifest changeRecords");
|
|
235
|
+
if (!Array.isArray(manifest.dirtyDirSummaries)) throw new Error("Invalid virtual workdir manifest dirtyDirSummaries");
|
|
222
236
|
if (manifest.formatVersion !== FILE_WORKDIR_MANIFEST_VERSION) throw new Error(`Unsupported virtual workdir file manifest version: expected ${FILE_WORKDIR_MANIFEST_VERSION}, got ${manifest.formatVersion}`);
|
|
223
237
|
}
|
|
224
238
|
function persistNode(contentDir, node) {
|
|
@@ -339,6 +353,62 @@ function restoreOrigin(record) {
|
|
|
339
353
|
hash: origin.hash
|
|
340
354
|
};
|
|
341
355
|
}
|
|
356
|
+
function serializeChangeRecord(record) {
|
|
357
|
+
return {
|
|
358
|
+
path: record.path,
|
|
359
|
+
previous: record.previous,
|
|
360
|
+
current: record.current,
|
|
361
|
+
source: record.source
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
function restoreChangeRecord(record) {
|
|
365
|
+
return {
|
|
366
|
+
path: record.path,
|
|
367
|
+
previous: record.previous === null ? null : {
|
|
368
|
+
...record.previous,
|
|
369
|
+
hash: record.previous.hash
|
|
370
|
+
},
|
|
371
|
+
current: record.current === null ? null : {
|
|
372
|
+
...record.current,
|
|
373
|
+
hash: record.current.hash
|
|
374
|
+
},
|
|
375
|
+
source: record.source
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
function serializeDirtyDirSummary(summary) {
|
|
379
|
+
return {
|
|
380
|
+
path: summary.path,
|
|
381
|
+
isDirty: summary.isDirty,
|
|
382
|
+
dirtyEntryCount: summary.dirtyEntryCount,
|
|
383
|
+
dirtyDescendantCount: summary.dirtyDescendantCount,
|
|
384
|
+
affectedNames: [...summary.affectedNames],
|
|
385
|
+
currentTreeHash: summary.currentTreeHash,
|
|
386
|
+
hashState: summary.hashState
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
function restoreDirtyDirSummary(summary) {
|
|
390
|
+
return {
|
|
391
|
+
path: summary.path,
|
|
392
|
+
isDirty: summary.isDirty,
|
|
393
|
+
dirtyEntryCount: readDirtyDirCount(summary.dirtyEntryCount, "dirtyEntryCount"),
|
|
394
|
+
dirtyDescendantCount: readDirtyDirCount(summary.dirtyDescendantCount, "dirtyDescendantCount"),
|
|
395
|
+
affectedNames: readDirtyDirAffectedNames(summary.affectedNames),
|
|
396
|
+
currentTreeHash: summary.currentTreeHash === null ? null : sha1(summary.currentTreeHash),
|
|
397
|
+
hashState: readDirtyDirHashState(summary.hashState)
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
function readDirtyDirCount(raw, field) {
|
|
401
|
+
if (typeof raw !== "number" || !Number.isInteger(raw) || raw < 0) throw new Error(`Invalid file workdir dirty dir summary ${field}`);
|
|
402
|
+
return raw;
|
|
403
|
+
}
|
|
404
|
+
function readDirtyDirAffectedNames(raw) {
|
|
405
|
+
if (!Array.isArray(raw) || raw.some((item) => typeof item !== "string")) throw new Error("Invalid file workdir dirty dir summary affectedNames");
|
|
406
|
+
return [...raw].sort((left, right) => left.localeCompare(right));
|
|
407
|
+
}
|
|
408
|
+
function readDirtyDirHashState(raw) {
|
|
409
|
+
if (raw === "stale" || raw === "materialized") return raw;
|
|
410
|
+
throw new Error(`Invalid file workdir dirty dir summary hashState: ${String(raw)}`);
|
|
411
|
+
}
|
|
342
412
|
function readPayload(contentDir, payloadRef) {
|
|
343
413
|
const path = getContentPath(contentDir, payloadRef);
|
|
344
414
|
if (!existsSync(path)) throw new Error(`Virtual workdir payload not found: ${payloadRef}`);
|
|
@@ -359,8 +429,5 @@ function writeBufferAtomic(path, value) {
|
|
|
359
429
|
function encodePathToken(value) {
|
|
360
430
|
return encodeURIComponent(value);
|
|
361
431
|
}
|
|
362
|
-
function decodePathToken(value) {
|
|
363
|
-
return decodeURIComponent(value);
|
|
364
|
-
}
|
|
365
432
|
//#endregion
|
|
366
|
-
export {
|
|
433
|
+
export { createFileVirtualWorkdirStateStore, deleteFileVirtualWorkdir, openFileVirtualWorkdir };
|
package/dist/workdir/file.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export { type
|
|
1
|
+
import { OpenFileVirtualWorkdirOptions, createFileVirtualWorkdirStateStore, deleteFileVirtualWorkdir, openFileVirtualWorkdir } from "./file-backend.mjs";
|
|
2
|
+
export { type OpenFileVirtualWorkdirOptions, createFileVirtualWorkdirStateStore, deleteFileVirtualWorkdir, openFileVirtualWorkdir };
|
package/dist/workdir/file.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export {
|
|
1
|
+
import { createFileVirtualWorkdirStateStore, deleteFileVirtualWorkdir, openFileVirtualWorkdir } from "./file-backend.mjs";
|
|
2
|
+
export { createFileVirtualWorkdirStateStore, deleteFileVirtualWorkdir, openFileVirtualWorkdir };
|
package/dist/workdir/ids.d.mts
CHANGED
package/dist/workdir/ids.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
//#region src/workdir/ids.ts
|
|
2
|
-
/** 根目录节点的固定 ID(每个
|
|
2
|
+
/** 根目录节点的固定 ID(每个 workdir 一致) */
|
|
3
3
|
const VIRTUAL_ROOT_NODE_ID = "root";
|
|
4
4
|
let nextNodeCounter = 1;
|
|
5
5
|
/**
|
|
6
|
-
* 分配新的
|
|
6
|
+
* 分配新的 workdir 节点 ID
|
|
7
7
|
*/
|
|
8
8
|
function createNodeId() {
|
|
9
9
|
const id = `node:${nextNodeCounter}`;
|
|
@@ -11,7 +11,7 @@ function createNodeId() {
|
|
|
11
11
|
return id;
|
|
12
12
|
}
|
|
13
13
|
/**
|
|
14
|
-
* 为仓库对象哈希派生稳定的
|
|
14
|
+
* 为仓库对象哈希派生稳定的 workdir 节点 ID(懒加载、未 copy 前复用)
|
|
15
15
|
*/
|
|
16
16
|
function originBackedNodeId(hash) {
|
|
17
17
|
return `origin:${hash}`;
|
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
import { createVirtualChangeLog } from "./change-log.mjs";
|
|
2
1
|
import { VIRTUAL_ROOT_NODE_ID } from "./ids.mjs";
|
|
3
2
|
import { createRootDirectoryNode } from "./nodes.mjs";
|
|
4
|
-
import { createVirtualWorkdirSessionId } from "./session-id.mjs";
|
|
5
|
-
import { openVirtualWorkdirSession } from "./session.mjs";
|
|
6
3
|
//#region src/workdir/memory-backend.ts
|
|
7
4
|
/**
|
|
8
5
|
* Virtual Workdir 内存状态存储
|
|
@@ -20,7 +17,8 @@ function createVirtualWorkdirMemoryStateStore(baseTree) {
|
|
|
20
17
|
const state = {
|
|
21
18
|
baseTree,
|
|
22
19
|
nodes: /* @__PURE__ */ new Map(),
|
|
23
|
-
|
|
20
|
+
changeRecords: /* @__PURE__ */ new Map(),
|
|
21
|
+
dirtyDirSummaries: /* @__PURE__ */ new Map()
|
|
24
22
|
};
|
|
25
23
|
resetState(state, baseTree);
|
|
26
24
|
return {
|
|
@@ -49,76 +47,62 @@ function createVirtualWorkdirMemoryStateStore(baseTree) {
|
|
|
49
47
|
deleteNode(id) {
|
|
50
48
|
state.nodes.delete(id);
|
|
51
49
|
},
|
|
52
|
-
appendChange(record) {
|
|
53
|
-
state.changeLog.append(record);
|
|
54
|
-
},
|
|
55
50
|
listChangeRecords() {
|
|
56
|
-
return state.
|
|
51
|
+
return Array.from(state.changeRecords.values()).sort((left, right) => left.path.localeCompare(right.path));
|
|
57
52
|
},
|
|
58
|
-
|
|
59
|
-
state.
|
|
53
|
+
getChangeRecord(path) {
|
|
54
|
+
return state.changeRecords.get(path) ?? null;
|
|
60
55
|
},
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* 创建内存版 Virtual Workdir backend
|
|
68
|
-
*
|
|
69
|
-
* @example
|
|
70
|
-
* ```ts
|
|
71
|
-
* const backend = createMemoryVirtualWorkdirBackend();
|
|
72
|
-
* const sessionId = backend.createSession({ baseTree: tree });
|
|
73
|
-
* const session = backend.openSession(repo.objects, sessionId);
|
|
74
|
-
* expect(session.baseTree).toBe(tree);
|
|
75
|
-
* ```
|
|
76
|
-
*/
|
|
77
|
-
function createMemoryVirtualWorkdirBackend() {
|
|
78
|
-
const sessions = /* @__PURE__ */ new Map();
|
|
79
|
-
return {
|
|
80
|
-
kind: "memory",
|
|
81
|
-
createSession(options) {
|
|
82
|
-
const sessionId = createVirtualWorkdirSessionId();
|
|
83
|
-
sessions.set(sessionId, createVirtualWorkdirMemoryStateStore(options.baseTree));
|
|
84
|
-
return sessionId;
|
|
56
|
+
setChangeRecord(record) {
|
|
57
|
+
state.changeRecords.set(record.path, record);
|
|
58
|
+
},
|
|
59
|
+
deleteChangeRecord(path) {
|
|
60
|
+
state.changeRecords.delete(path);
|
|
85
61
|
},
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
if (store === void 0) throw new Error(`Virtual workdir session not found: ${sessionId}`);
|
|
89
|
-
return openVirtualWorkdirSession(source, store);
|
|
62
|
+
listDirtyDirSummaries() {
|
|
63
|
+
return Array.from(state.dirtyDirSummaries.values()).sort((left, right) => left.path.localeCompare(right.path));
|
|
90
64
|
},
|
|
91
|
-
|
|
92
|
-
|
|
65
|
+
getDirtyDirSummary(path) {
|
|
66
|
+
return state.dirtyDirSummaries.get(path) ?? null;
|
|
93
67
|
},
|
|
94
|
-
|
|
95
|
-
|
|
68
|
+
setDirtyDirSummary(summary) {
|
|
69
|
+
state.dirtyDirSummaries.set(summary.path, summary);
|
|
70
|
+
},
|
|
71
|
+
deleteDirtyDirSummary(path) {
|
|
72
|
+
state.dirtyDirSummaries.delete(path);
|
|
73
|
+
},
|
|
74
|
+
reset(nextBaseTree) {
|
|
75
|
+
resetState(state, nextBaseTree);
|
|
96
76
|
}
|
|
97
77
|
};
|
|
98
78
|
}
|
|
99
79
|
function resetState(state, baseTree) {
|
|
100
80
|
state.baseTree = baseTree;
|
|
101
81
|
state.nodes.clear();
|
|
82
|
+
state.changeRecords.clear();
|
|
83
|
+
state.dirtyDirSummaries.clear();
|
|
102
84
|
state.nodes.set(VIRTUAL_ROOT_NODE_ID, createRootDirectoryNode(baseTree));
|
|
103
|
-
state.changeLog.clear();
|
|
104
85
|
}
|
|
105
86
|
function snapshotState(state) {
|
|
106
87
|
const nodes = /* @__PURE__ */ new Map();
|
|
107
|
-
for (const [nodeId, node] of state.nodes) nodes.set(nodeId,
|
|
88
|
+
for (const [nodeId, node] of state.nodes) nodes.set(nodeId, cloneWorkdirNode(node));
|
|
108
89
|
return {
|
|
109
90
|
baseTree: state.baseTree,
|
|
110
91
|
nodes,
|
|
111
|
-
|
|
92
|
+
changeRecords: new Map(state.changeRecords),
|
|
93
|
+
dirtyDirSummaries: new Map(state.dirtyDirSummaries)
|
|
112
94
|
};
|
|
113
95
|
}
|
|
114
96
|
function restoreState(state, snapshot) {
|
|
115
97
|
state.baseTree = snapshot.baseTree;
|
|
116
98
|
state.nodes.clear();
|
|
117
|
-
|
|
118
|
-
state.
|
|
119
|
-
for (const
|
|
99
|
+
state.changeRecords.clear();
|
|
100
|
+
state.dirtyDirSummaries.clear();
|
|
101
|
+
for (const [nodeId, node] of snapshot.nodes) state.nodes.set(nodeId, cloneWorkdirNode(node));
|
|
102
|
+
for (const [path, record] of snapshot.changeRecords) state.changeRecords.set(path, record);
|
|
103
|
+
for (const [path, summary] of snapshot.dirtyDirSummaries) state.dirtyDirSummaries.set(path, summary);
|
|
120
104
|
}
|
|
121
|
-
function
|
|
105
|
+
function cloneWorkdirNode(node) {
|
|
122
106
|
if (node.state.kind === "directory") return {
|
|
123
107
|
id: node.id,
|
|
124
108
|
origin: node.origin,
|
|
@@ -156,4 +140,4 @@ function cloneSessionNode(node) {
|
|
|
156
140
|
};
|
|
157
141
|
}
|
|
158
142
|
//#endregion
|
|
159
|
-
export {
|
|
143
|
+
export { createVirtualWorkdirMemoryStateStore };
|
|
@@ -1,3 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
export { createMemoryVirtualWorkdirBackend, createVirtualWorkdirSession, openVirtualWorkdirSession };
|
|
1
|
+
import { createVirtualWorkdir, openVirtualWorkdir } from "./workdir.mjs";
|
|
2
|
+
export { createVirtualWorkdir, openVirtualWorkdir };
|
package/dist/workdir/memory.mjs
CHANGED
|
@@ -1,3 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
export { createMemoryVirtualWorkdirBackend, createVirtualWorkdirSession, openVirtualWorkdirSession };
|
|
1
|
+
import { createVirtualWorkdir, openVirtualWorkdir } from "./workdir.mjs";
|
|
2
|
+
export { createVirtualWorkdir, openVirtualWorkdir };
|
package/dist/workdir/nodes.d.mts
CHANGED
|
@@ -6,7 +6,7 @@ import { DirectoryOverlay } from "./overlay.mjs";
|
|
|
6
6
|
/** Blob / 符号链接在 origin 与 state 中使用的 mode */
|
|
7
7
|
type BlobObjectMode = "100644" | "100755" | "120000";
|
|
8
8
|
/**
|
|
9
|
-
* 节点来源(repo 对象或纯
|
|
9
|
+
* 节点来源(repo 对象或纯 workdir 新建)
|
|
10
10
|
*/
|
|
11
11
|
type NodeOrigin = {
|
|
12
12
|
readonly kind: "none";
|
|
@@ -21,7 +21,7 @@ type NodeOrigin = {
|
|
|
21
21
|
/**
|
|
22
22
|
* 目录节点当前状态
|
|
23
23
|
*
|
|
24
|
-
* `overlay` 表达
|
|
24
|
+
* `overlay` 表达 workdir 层增删改;子项 nodeId 通过 overlay 合成与懒加载解析。
|
|
25
25
|
*/
|
|
26
26
|
interface DirectoryNodeState {
|
|
27
27
|
readonly kind: "directory";
|
|
@@ -44,14 +44,14 @@ interface SymlinkNodeState {
|
|
|
44
44
|
readonly mode: "120000";
|
|
45
45
|
readonly target?: Buffer;
|
|
46
46
|
}
|
|
47
|
-
type
|
|
47
|
+
type WorkdirNodeState = DirectoryNodeState | FileNodeState | SymlinkNodeState;
|
|
48
48
|
/**
|
|
49
|
-
*
|
|
49
|
+
* 完整的 workdir 节点记录
|
|
50
50
|
*/
|
|
51
|
-
interface
|
|
51
|
+
interface WorkdirNode {
|
|
52
52
|
readonly id: NodeId;
|
|
53
53
|
readonly origin: NodeOrigin;
|
|
54
|
-
readonly state:
|
|
54
|
+
readonly state: WorkdirNodeState;
|
|
55
55
|
}
|
|
56
56
|
//#endregion
|
|
57
|
-
export {
|
|
57
|
+
export { WorkdirNode };
|
package/dist/workdir/nodes.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { VIRTUAL_ROOT_NODE_ID } from "./ids.mjs";
|
|
|
2
2
|
import { cloneDirectoryOverlay, createEmptyDirectoryOverlay } from "./overlay.mjs";
|
|
3
3
|
//#region src/workdir/nodes.ts
|
|
4
4
|
/**
|
|
5
|
-
* Virtual Workdir
|
|
5
|
+
* Virtual Workdir 节点状态模型
|
|
6
6
|
*
|
|
7
7
|
* 节点身份(nodeId)与目录路径绑定分离;origin 描述 repo-backed 来源。
|
|
8
8
|
*/
|
|
@@ -29,12 +29,6 @@ function nodeHasRepoOrigin(node) {
|
|
|
29
29
|
return node.origin.kind === "repo-tree" || node.origin.kind === "repo-blob";
|
|
30
30
|
}
|
|
31
31
|
/**
|
|
32
|
-
* 目录 overlay 是否有 session 层修改
|
|
33
|
-
*/
|
|
34
|
-
function isDirectoryOverlayDirty(overlay) {
|
|
35
|
-
return overlay.addedEntries.size > 0 || overlay.deletedNames.size > 0;
|
|
36
|
-
}
|
|
37
|
-
/**
|
|
38
32
|
* 将节点状态恢复到 origin 语义(丢弃 materialize / 目录 overlay)
|
|
39
33
|
*
|
|
40
34
|
* 无 repo origin 时返回原状态引用,由上层决定是否抛错。
|
|
@@ -69,7 +63,7 @@ function revertNodeState(node) {
|
|
|
69
63
|
/**
|
|
70
64
|
* 为 `copy` 创建新节点:共享 origin,目录为浅复制(子项绑定保留,但 nodeId 为新)
|
|
71
65
|
*/
|
|
72
|
-
function
|
|
66
|
+
function cloneWorkdirNodeForCopy(source, newId) {
|
|
73
67
|
const origin = source.origin;
|
|
74
68
|
if (source.state.kind === "directory") return {
|
|
75
69
|
id: newId,
|
|
@@ -109,4 +103,4 @@ function cloneSessionNodeForCopy(source, newId) {
|
|
|
109
103
|
};
|
|
110
104
|
}
|
|
111
105
|
//#endregion
|
|
112
|
-
export {
|
|
106
|
+
export { cloneWorkdirNodeForCopy, createRootDirectoryNode, revertNodeState };
|
|
@@ -2,12 +2,12 @@ import { NodeId } from "./ids.mjs";
|
|
|
2
2
|
|
|
3
3
|
//#region src/workdir/overlay.d.ts
|
|
4
4
|
/**
|
|
5
|
-
* 目录 overlay 状态(挂在目录
|
|
5
|
+
* 目录 overlay 状态(挂在目录 WorkdirNode 上)
|
|
6
6
|
*/
|
|
7
7
|
interface DirectoryOverlay {
|
|
8
|
-
/**
|
|
8
|
+
/** workdir 新增或覆盖:条目名 -> 绑定的 nodeId */
|
|
9
9
|
readonly addedEntries: Map<string, NodeId>;
|
|
10
|
-
/**
|
|
10
|
+
/** workdir 删除的 origin/先前条目名(tombstone) */
|
|
11
11
|
readonly deletedNames: Set<string>;
|
|
12
12
|
}
|
|
13
13
|
//#endregion
|