nano-git 0.2.0 → 0.2.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.
Files changed (40) hide show
  1. package/dist/core/errors.d.mts +71 -1
  2. package/dist/core/errors.mjs +99 -1
  3. package/dist/errors.d.mts +2 -2
  4. package/dist/errors.mjs +2 -2
  5. package/dist/log/index.d.mts +3 -0
  6. package/dist/log/index.mjs +2 -0
  7. package/dist/log/types.d.mts +71 -0
  8. package/dist/log/walk.d.mts +37 -0
  9. package/dist/log/walk.mjs +274 -0
  10. package/dist/workdir/change-log.d.mts +25 -0
  11. package/dist/workdir/change-log.mjs +69 -0
  12. package/dist/workdir/core.d.mts +188 -0
  13. package/dist/workdir/core.mjs +2 -0
  14. package/dist/workdir/file-backend.d.mts +22 -0
  15. package/dist/workdir/file-backend.mjs +366 -0
  16. package/dist/workdir/file.d.mts +2 -0
  17. package/dist/workdir/file.mjs +2 -0
  18. package/dist/workdir/ids.d.mts +7 -0
  19. package/dist/workdir/ids.mjs +20 -0
  20. package/dist/workdir/memory-backend.d.mts +17 -0
  21. package/dist/workdir/memory-backend.mjs +159 -0
  22. package/dist/workdir/memory.d.mts +3 -0
  23. package/dist/workdir/memory.mjs +3 -0
  24. package/dist/workdir/nodes.d.mts +57 -0
  25. package/dist/workdir/nodes.mjs +112 -0
  26. package/dist/workdir/origin.mjs +55 -0
  27. package/dist/workdir/overlay.d.mts +14 -0
  28. package/dist/workdir/overlay.mjs +95 -0
  29. package/dist/workdir/path.mjs +70 -0
  30. package/dist/workdir/session-id.mjs +18 -0
  31. package/dist/workdir/session-internal.mjs +141 -0
  32. package/dist/workdir/session.d.mts +30 -0
  33. package/dist/workdir/session.mjs +407 -0
  34. package/dist/workdir/sqlite-backend.d.mts +30 -0
  35. package/dist/workdir/sqlite-backend.mjs +363 -0
  36. package/dist/workdir/sqlite.d.mts +2 -0
  37. package/dist/workdir/sqlite.mjs +2 -0
  38. package/dist/workdir/state-store.d.mts +44 -0
  39. package/dist/workdir/write-tree.mjs +97 -0
  40. package/package.json +13 -3
@@ -0,0 +1,141 @@
1
+ import { VirtualNotDirectoryError } from "../core/errors.mjs";
2
+ import { VIRTUAL_ROOT_NODE_ID, originBackedNodeId } from "./ids.mjs";
3
+ import { mergeDirectoryChildren } from "./overlay.mjs";
4
+ import { readRepoTree, treeEntryToNodeOrigin } from "./origin.mjs";
5
+ import { assertValidVirtualPath, joinPath, splitPathSegments } from "./path.mjs";
6
+ //#region src/workdir/session-internal.ts
7
+ /**
8
+ * Virtual Workdir session 内部共享逻辑
9
+ *
10
+ * 供 session.ts 与 write-tree.ts 复用:
11
+ * - 路径解析(resolvePath)
12
+ * - 目录子项展开(listDirectoryChildren)
13
+ * - origin 节点懒注册(ensureNodeFromTreeEntry)
14
+ */
15
+ function getRootNode(state) {
16
+ const root = state.getNode(VIRTUAL_ROOT_NODE_ID);
17
+ if (root === null) throw new Error("Virtual workdir session is missing root node");
18
+ return root;
19
+ }
20
+ /**
21
+ * 从根目录沿路径分段解析到目标节点
22
+ */
23
+ function resolvePath(source, state, path) {
24
+ assertValidVirtualPath(path);
25
+ const segments = splitPathSegments(path);
26
+ let current = getRootNode(state);
27
+ let currentPath = "";
28
+ for (const segment of segments) {
29
+ if (current.state.kind !== "directory") return {
30
+ found: false,
31
+ node: null
32
+ };
33
+ const child = listDirectoryChildren(source, state, current, currentPath).find((c) => c.name === segment);
34
+ if (child === void 0) return {
35
+ found: false,
36
+ node: null
37
+ };
38
+ currentPath = joinPath(currentPath === "" ? null : currentPath, segment);
39
+ const childNode = state.getNode(child.nodeId);
40
+ if (childNode === null) return {
41
+ found: false,
42
+ node: null
43
+ };
44
+ current = childNode;
45
+ }
46
+ return {
47
+ found: true,
48
+ node: current
49
+ };
50
+ }
51
+ /**
52
+ * 根据父节点与子条目名解析子节点
53
+ */
54
+ function resolveChild(source, state, parentNode, parentPath, name) {
55
+ if (parentNode.state.kind !== "directory") return {
56
+ found: false,
57
+ node: null
58
+ };
59
+ const child = listDirectoryChildren(source, state, parentNode, parentPath).find((c) => c.name === name);
60
+ if (child === void 0) return {
61
+ found: false,
62
+ node: null
63
+ };
64
+ const childNode = state.getNode(child.nodeId);
65
+ if (childNode === null) return {
66
+ found: false,
67
+ node: null
68
+ };
69
+ return {
70
+ found: true,
71
+ node: childNode
72
+ };
73
+ }
74
+ /**
75
+ * 确保 origin 条目对应的 session 节点已懒注册
76
+ */
77
+ function ensureNodeFromTreeEntry(state, entry) {
78
+ const id = originBackedNodeId(entry.hash);
79
+ if (state.getNode(id) === null) {
80
+ const origin = treeEntryToNodeOrigin(entry);
81
+ let nodeState;
82
+ if (entry.mode === "40000") nodeState = {
83
+ kind: "directory",
84
+ overlay: {
85
+ addedEntries: /* @__PURE__ */ new Map(),
86
+ deletedNames: /* @__PURE__ */ new Set()
87
+ }
88
+ };
89
+ else if (entry.mode === "120000") nodeState = {
90
+ kind: "symlink",
91
+ mode: "120000"
92
+ };
93
+ else nodeState = {
94
+ kind: "file",
95
+ mode: entry.mode === "100755" ? "100755" : "100644"
96
+ };
97
+ state.setNode({
98
+ id,
99
+ origin,
100
+ state: nodeState
101
+ });
102
+ }
103
+ return id;
104
+ }
105
+ /**
106
+ * 展开目录的完整子项列表(origin + overlay 合成)
107
+ */
108
+ function listDirectoryChildren(source, state, dirNode, dirPath) {
109
+ if (dirNode.state.kind !== "directory") throw new VirtualNotDirectoryError(dirPath);
110
+ let originChildren = [];
111
+ if (dirNode.origin.kind === "repo-tree") originChildren = readRepoTree(source, dirNode.origin.hash, dirPath).entries.map((entry) => {
112
+ const childNodeId = ensureNodeFromTreeEntry(state, entry);
113
+ return {
114
+ name: entry.name,
115
+ mode: entry.mode,
116
+ nodeId: childNodeId
117
+ };
118
+ });
119
+ const addedModes = buildAddedModes(state, dirNode);
120
+ return mergeDirectoryChildren(originChildren, dirNode.state.overlay, addedModes);
121
+ }
122
+ /**
123
+ * 构建 overlay addedEntries 对应的 mode map
124
+ */
125
+ function buildAddedModes(state, dirNode) {
126
+ if (dirNode.state.kind !== "directory") return /* @__PURE__ */ new Map();
127
+ const addedModes = /* @__PURE__ */ new Map();
128
+ for (const name of dirNode.state.overlay.addedEntries.keys()) {
129
+ const nodeId = dirNode.state.overlay.addedEntries.get(name);
130
+ const node = state.getNode(nodeId);
131
+ if (node !== null) addedModes.set(name, sessionNodeMode(node));
132
+ }
133
+ return addedModes;
134
+ }
135
+ function sessionNodeMode(node) {
136
+ if (node.state.kind === "directory") return "40000";
137
+ if (node.state.kind === "symlink") return "120000";
138
+ return node.state.mode;
139
+ }
140
+ //#endregion
141
+ export { listDirectoryChildren, resolveChild, resolvePath };
@@ -0,0 +1,30 @@
1
+ import { ObjectDatabase } from "../core/types/odb.mjs";
2
+ import { CreateVirtualWorkdirSessionOptions, VirtualWorkdirSession } from "./core.mjs";
3
+ import { VirtualWorkdirStateStore } from "./state-store.mjs";
4
+
5
+ //#region src/workdir/session.d.ts
6
+ /**
7
+ * 基于 ObjectDatabase 创建 VirtualWorkdirSession
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * const repo = createMemoryRepository();
12
+ * const tree = repo.createTree([]);
13
+ * const session = createVirtualWorkdirSession(repo.objects, { baseTree: tree });
14
+ * expect(session.readdir()).toEqual([]);
15
+ * ```
16
+ */
17
+ declare function createVirtualWorkdirSession(source: ObjectDatabase, options: CreateVirtualWorkdirSessionOptions): VirtualWorkdirSession;
18
+ /**
19
+ * 基于已有状态存储打开 session
20
+ *
21
+ * @example
22
+ * ```ts
23
+ * const store = createVirtualWorkdirMemoryStateStore(tree);
24
+ * const session = openVirtualWorkdirSession(repo.objects, store);
25
+ * expect(session.baseTree).toBe(tree);
26
+ * ```
27
+ */
28
+ declare function openVirtualWorkdirSession(source: ObjectDatabase, state: VirtualWorkdirStateStore): VirtualWorkdirSession;
29
+ //#endregion
30
+ export { createVirtualWorkdirSession, openVirtualWorkdirSession };
@@ -0,0 +1,407 @@
1
+ import { VirtualNotDirectoryError, VirtualNotFileError, VirtualNotSymlinkError, VirtualPathAlreadyExistsError, VirtualPathNotFoundError, VirtualRevertNotSupportedError } from "../core/errors.mjs";
2
+ import { mapInternalChangesToVirtualChanges } from "./change-log.mjs";
3
+ import { VIRTUAL_ROOT_NODE_ID, createNodeId } from "./ids.mjs";
4
+ import { overlayBindEntry, overlayRenameEntry, overlayTombstoneEntry } from "./overlay.mjs";
5
+ import { cloneSessionNodeForCopy, revertNodeState } from "./nodes.mjs";
6
+ import { modeToVirtualEntryKind, readRepoBlobContent } from "./origin.mjs";
7
+ import { assertValidVirtualPath, baseName, normalizeDirectoryPath, parentPath } from "./path.mjs";
8
+ import { listDirectoryChildren, resolveChild, resolvePath } from "./session-internal.mjs";
9
+ import { writeTreeFromSession } from "./write-tree.mjs";
10
+ import { createVirtualWorkdirMemoryStateStore } from "./memory-backend.mjs";
11
+ //#region src/workdir/session.ts
12
+ /**
13
+ * VirtualWorkdirSession 行为编排
14
+ *
15
+ * Phase 5:完整文件/目录 rename 与 copy 语义。
16
+ */
17
+ /**
18
+ * 基于 ObjectDatabase 创建 VirtualWorkdirSession
19
+ *
20
+ * @example
21
+ * ```ts
22
+ * const repo = createMemoryRepository();
23
+ * const tree = repo.createTree([]);
24
+ * const session = createVirtualWorkdirSession(repo.objects, { baseTree: tree });
25
+ * expect(session.readdir()).toEqual([]);
26
+ * ```
27
+ */
28
+ function createVirtualWorkdirSession(source, options) {
29
+ return openVirtualWorkdirSession(source, createVirtualWorkdirMemoryStateStore(options.baseTree));
30
+ }
31
+ /**
32
+ * 基于已有状态存储打开 session
33
+ *
34
+ * @example
35
+ * ```ts
36
+ * const store = createVirtualWorkdirMemoryStateStore(tree);
37
+ * const session = openVirtualWorkdirSession(repo.objects, store);
38
+ * expect(session.baseTree).toBe(tree);
39
+ * ```
40
+ */
41
+ function openVirtualWorkdirSession(source, state) {
42
+ return {
43
+ get baseTree() {
44
+ return state.readBaseTree();
45
+ },
46
+ exists(path) {
47
+ if (path === "") return true;
48
+ return resolvePath(source, state, path).found;
49
+ },
50
+ stat(path) {
51
+ if (path === "") return statDirectoryNode(getRootNode(state));
52
+ const resolved = resolvePath(source, state, path);
53
+ if (!resolved.found || resolved.node === null) return null;
54
+ return statNode(source, resolved.node, path);
55
+ },
56
+ readdir(dirPath) {
57
+ const normalized = normalizeDirectoryPath(dirPath);
58
+ const resolved = normalized === "" ? {
59
+ found: true,
60
+ node: getRootNode(state)
61
+ } : resolvePath(source, state, normalized);
62
+ if (!resolved.found || resolved.node === null) throw new VirtualPathNotFoundError(normalized);
63
+ if (resolved.node.state.kind !== "directory") throw new VirtualNotDirectoryError(normalized);
64
+ return listDirectoryChildren(source, state, resolved.node, normalized).map((child) => ({
65
+ name: child.name,
66
+ kind: modeToVirtualEntryKind(child.mode),
67
+ mode: child.mode
68
+ }));
69
+ },
70
+ readFile(path) {
71
+ assertValidVirtualPath(path);
72
+ const resolved = resolvePath(source, state, path);
73
+ if (!resolved.found || resolved.node === null) throw new VirtualPathNotFoundError(path);
74
+ const node = resolved.node;
75
+ if (node.state.kind === "file") {
76
+ if (node.state.content !== void 0) return node.state.content;
77
+ if (node.origin.kind === "repo-blob") return readRepoBlobContent(source, node.origin.hash, path);
78
+ throw new VirtualPathNotFoundError(path);
79
+ }
80
+ if (node.state.kind === "symlink") throw new VirtualNotFileError(path);
81
+ throw new VirtualNotFileError(path);
82
+ },
83
+ readLink(path) {
84
+ assertValidVirtualPath(path);
85
+ const resolved = resolvePath(source, state, path);
86
+ if (!resolved.found || resolved.node === null) throw new VirtualPathNotFoundError(path);
87
+ const node = resolved.node;
88
+ if (node.state.kind !== "symlink") throw new VirtualNotSymlinkError(path);
89
+ if (node.state.target !== void 0) return node.state.target.toString("utf-8");
90
+ if (node.origin.kind === "repo-blob") return readRepoBlobContent(source, node.origin.hash, path).toString("utf-8");
91
+ throw new VirtualPathNotFoundError(path);
92
+ },
93
+ mkdir(path) {
94
+ runInWriteTransaction(state, () => {
95
+ assertValidVirtualPath(path);
96
+ const parent = parentPath(path);
97
+ const name = baseName(path);
98
+ const parentResolved = parent !== null ? resolvePath(source, state, parent) : {
99
+ found: true,
100
+ node: getRootNode(state)
101
+ };
102
+ if (!parentResolved.found || parentResolved.node === null) throw new VirtualPathNotFoundError(parent ?? path);
103
+ const parentNode = parentResolved.node;
104
+ if (parentNode.state.kind !== "directory") throw new VirtualNotDirectoryError(parent ?? path);
105
+ const existing = resolveChild(source, state, parentNode, parent ?? "", name);
106
+ if (existing.found && existing.node !== null) throw new VirtualPathAlreadyExistsError(path);
107
+ const nodeId = createNodeId();
108
+ const newNode = {
109
+ id: nodeId,
110
+ origin: { kind: "none" },
111
+ state: {
112
+ kind: "directory",
113
+ overlay: {
114
+ addedEntries: /* @__PURE__ */ new Map(),
115
+ deletedNames: /* @__PURE__ */ new Set()
116
+ }
117
+ }
118
+ };
119
+ state.setNode(newNode);
120
+ updateParentOverlay(state, parentNode.id, overlayBindEntry(parentNode.state.overlay, name, nodeId));
121
+ state.appendChange({
122
+ op: "add",
123
+ path
124
+ });
125
+ });
126
+ },
127
+ writeFile(path, content, options) {
128
+ runInWriteTransaction(state, () => {
129
+ assertValidVirtualPath(path);
130
+ const mode = options?.mode ?? "100644";
131
+ const parent = parentPath(path);
132
+ const name = baseName(path);
133
+ const parentResolved = parent !== null ? resolvePath(source, state, parent) : {
134
+ found: true,
135
+ node: getRootNode(state)
136
+ };
137
+ if (!parentResolved.found || parentResolved.node === null) throw new VirtualPathNotFoundError(parent ?? path);
138
+ const parentNode = parentResolved.node;
139
+ if (parentNode.state.kind !== "directory") throw new VirtualNotDirectoryError(parent ?? path);
140
+ const existing = resolveChild(source, state, parentNode, parent ?? "", name);
141
+ if (existing.found && existing.node !== null) {
142
+ if (existing.node.state.kind === "directory") throw new VirtualNotFileError(path);
143
+ }
144
+ const isNew = !existing.found || existing.node === null;
145
+ const nodeId = existing.found ? existing.node.id : createNodeId();
146
+ const fileNode = {
147
+ id: nodeId,
148
+ origin: existing.found ? existing.node.origin : { kind: "none" },
149
+ state: {
150
+ kind: "file",
151
+ mode,
152
+ content
153
+ }
154
+ };
155
+ state.setNode(fileNode);
156
+ updateParentOverlay(state, parentNode.id, overlayBindEntry(parentNode.state.overlay, name, nodeId));
157
+ state.appendChange({
158
+ op: isNew ? "add" : "modify",
159
+ path
160
+ });
161
+ });
162
+ },
163
+ writeLink(path, target) {
164
+ runInWriteTransaction(state, () => {
165
+ assertValidVirtualPath(path);
166
+ const parent = parentPath(path);
167
+ const name = baseName(path);
168
+ const parentResolved = parent !== null ? resolvePath(source, state, parent) : {
169
+ found: true,
170
+ node: getRootNode(state)
171
+ };
172
+ if (!parentResolved.found || parentResolved.node === null) throw new VirtualPathNotFoundError(parent ?? path);
173
+ const parentNode = parentResolved.node;
174
+ if (parentNode.state.kind !== "directory") throw new VirtualNotDirectoryError(parent ?? path);
175
+ const existing = resolveChild(source, state, parentNode, parent ?? "", name);
176
+ if (existing.found && existing.node !== null) {
177
+ if (existing.node.state.kind === "directory") throw new VirtualNotFileError(path);
178
+ }
179
+ const isNew = !existing.found || existing.node === null;
180
+ const nodeId = existing.found ? existing.node.id : createNodeId();
181
+ const linkNode = {
182
+ id: nodeId,
183
+ origin: existing.found ? existing.node.origin : { kind: "none" },
184
+ state: {
185
+ kind: "symlink",
186
+ mode: "120000",
187
+ target: Buffer.from(target)
188
+ }
189
+ };
190
+ state.setNode(linkNode);
191
+ updateParentOverlay(state, parentNode.id, overlayBindEntry(parentNode.state.overlay, name, nodeId));
192
+ state.appendChange({
193
+ op: isNew ? "add" : "modify",
194
+ path
195
+ });
196
+ });
197
+ },
198
+ delete(path) {
199
+ runInWriteTransaction(state, () => {
200
+ assertValidVirtualPath(path);
201
+ const parent = parentPath(path);
202
+ const name = baseName(path);
203
+ const parentResolved = parent !== null ? resolvePath(source, state, parent) : {
204
+ found: true,
205
+ node: getRootNode(state)
206
+ };
207
+ if (!parentResolved.found || parentResolved.node === null) throw new VirtualPathNotFoundError(parent ?? path);
208
+ const parentNode = parentResolved.node;
209
+ if (parentNode.state.kind !== "directory") throw new VirtualNotDirectoryError(parent ?? path);
210
+ const existing = resolveChild(source, state, parentNode, parent ?? "", name);
211
+ if (!existing.found || existing.node === null) throw new VirtualPathNotFoundError(path);
212
+ updateParentOverlay(state, parentNode.id, overlayTombstoneEntry(parentNode.state.overlay, name));
213
+ state.appendChange({
214
+ op: "delete",
215
+ path
216
+ });
217
+ });
218
+ },
219
+ rename(from, to) {
220
+ runInWriteTransaction(state, () => {
221
+ assertValidVirtualPath(from);
222
+ assertValidVirtualPath(to);
223
+ if (from === to) return;
224
+ const fromParent = parentPath(from);
225
+ const fromName = baseName(from);
226
+ const fromParentResolved = fromParent !== null ? resolvePath(source, state, fromParent) : {
227
+ found: true,
228
+ node: getRootNode(state)
229
+ };
230
+ if (!fromParentResolved.found || fromParentResolved.node === null) throw new VirtualPathNotFoundError(from);
231
+ const fromParentNode = fromParentResolved.node;
232
+ if (fromParentNode.state.kind !== "directory") throw new VirtualNotDirectoryError(from);
233
+ const fromChild = resolveChild(source, state, fromParentNode, fromParent ?? "", fromName);
234
+ if (!fromChild.found || fromChild.node === null) throw new VirtualPathNotFoundError(from);
235
+ const sourceNode = fromChild.node;
236
+ const toParent = parentPath(to);
237
+ const toName = baseName(to);
238
+ const toParentResolved = toParent !== null ? resolvePath(source, state, toParent) : {
239
+ found: true,
240
+ node: getRootNode(state)
241
+ };
242
+ if (!toParentResolved.found || toParentResolved.node === null) throw new VirtualPathNotFoundError(to);
243
+ const toParentNode = toParentResolved.node;
244
+ if (toParentNode.state.kind !== "directory") throw new VirtualNotDirectoryError(to);
245
+ const toExisting = resolveChild(source, state, toParentNode, toParent ?? "", toName);
246
+ if (toExisting.found && toExisting.node !== null) throw new VirtualPathAlreadyExistsError(to);
247
+ if (sourceNode.state.kind === "directory") {
248
+ const toPath = to;
249
+ const fromPath = from;
250
+ if (toPath.startsWith(fromPath + "/") || toPath === fromPath) throw new Error(`Cannot rename '${from}' to '${to}': destination is a subdirectory of source`);
251
+ }
252
+ if (fromParentNode.id === toParentNode.id) updateParentOverlay(state, fromParentNode.id, overlayRenameEntry(fromParentNode.state.overlay, fromName, toName, sourceNode.id));
253
+ else {
254
+ updateParentOverlay(state, fromParentNode.id, overlayTombstoneEntry(fromParentNode.state.overlay, fromName));
255
+ updateParentOverlay(state, toParentNode.id, overlayBindEntry(toParentNode.state.overlay, toName, sourceNode.id));
256
+ }
257
+ state.appendChange({
258
+ op: "rename",
259
+ from,
260
+ to
261
+ });
262
+ });
263
+ },
264
+ copy(from, to) {
265
+ runInWriteTransaction(state, () => {
266
+ assertValidVirtualPath(from);
267
+ assertValidVirtualPath(to);
268
+ if (from === to) throw new VirtualPathAlreadyExistsError(to);
269
+ const fromParent = parentPath(from);
270
+ const fromName = baseName(from);
271
+ const fromParentResolved = fromParent !== null ? resolvePath(source, state, fromParent) : {
272
+ found: true,
273
+ node: getRootNode(state)
274
+ };
275
+ if (!fromParentResolved.found || fromParentResolved.node === null) throw new VirtualPathNotFoundError(from);
276
+ const fromParentNode = fromParentResolved.node;
277
+ if (fromParentNode.state.kind !== "directory") throw new VirtualNotDirectoryError(from);
278
+ const fromChild = resolveChild(source, state, fromParentNode, fromParent ?? "", fromName);
279
+ if (!fromChild.found || fromChild.node === null) throw new VirtualPathNotFoundError(from);
280
+ const sourceNode = fromChild.node;
281
+ const toParent = parentPath(to);
282
+ const toName = baseName(to);
283
+ const toParentResolved = toParent !== null ? resolvePath(source, state, toParent) : {
284
+ found: true,
285
+ node: getRootNode(state)
286
+ };
287
+ if (!toParentResolved.found || toParentResolved.node === null) throw new VirtualPathNotFoundError(to);
288
+ const toParentNode = toParentResolved.node;
289
+ if (toParentNode.state.kind !== "directory") throw new VirtualNotDirectoryError(to);
290
+ const toExisting = resolveChild(source, state, toParentNode, toParent ?? "", toName);
291
+ if (toExisting.found && toExisting.node !== null) throw new VirtualPathAlreadyExistsError(to);
292
+ const newNodeId = cloneNodeGraphForCopy(source, state, sourceNode, from);
293
+ updateParentOverlay(state, toParentNode.id, overlayBindEntry(toParentNode.state.overlay, toName, newNodeId));
294
+ state.appendChange({
295
+ op: "copy",
296
+ from,
297
+ to
298
+ });
299
+ });
300
+ },
301
+ revert(path) {
302
+ runInWriteTransaction(state, () => {
303
+ assertValidVirtualPath(path);
304
+ const resolved = resolvePath(source, state, path);
305
+ if (!resolved.found || resolved.node === null) throw new VirtualPathNotFoundError(path);
306
+ const node = resolved.node;
307
+ const reverted = revertNodeState(node);
308
+ if (reverted === node) throw new VirtualRevertNotSupportedError(path);
309
+ state.setNode(reverted);
310
+ state.appendChange({
311
+ op: "revert",
312
+ path
313
+ });
314
+ });
315
+ },
316
+ writeTree() {
317
+ return writeTreeFromSession(source, state);
318
+ },
319
+ reset(baseTree) {
320
+ runInWriteTransaction(state, () => {
321
+ state.reset(baseTree);
322
+ });
323
+ },
324
+ listChanges() {
325
+ return mapInternalChangesToVirtualChanges(state.listChangeRecords());
326
+ }
327
+ };
328
+ }
329
+ function getRootNode(state) {
330
+ const root = state.getNode(VIRTUAL_ROOT_NODE_ID);
331
+ if (root === null) throw new Error("Virtual workdir session is missing root node");
332
+ return root;
333
+ }
334
+ function runInWriteTransaction(state, fn) {
335
+ return state.transact(fn);
336
+ }
337
+ /**
338
+ * 更新父节点的 overlay(创建新节点对象替代 Map 中的旧引用)
339
+ */
340
+ function updateParentOverlay(state, parentId, newOverlay) {
341
+ const parentNode = state.getNode(parentId);
342
+ if (parentNode === null || parentNode.state.kind !== "directory") throw new Error("updateParentOverlay: parent is not a directory");
343
+ state.setNode({
344
+ ...parentNode,
345
+ state: {
346
+ ...parentNode.state,
347
+ overlay: newOverlay
348
+ }
349
+ });
350
+ }
351
+ function statNode(source, node, path) {
352
+ if (node.state.kind === "directory") return statDirectoryNode(node);
353
+ if (node.state.kind === "symlink") {
354
+ const hash = node.origin.kind === "repo-blob" ? node.origin.hash : null;
355
+ let size = 0;
356
+ if (node.state.target !== void 0) size = node.state.target.length;
357
+ else if (hash !== null) size = readRepoBlobContent(source, hash, path).length;
358
+ return {
359
+ kind: "symlink",
360
+ mode: "120000",
361
+ size,
362
+ hash
363
+ };
364
+ }
365
+ const hash = node.origin.kind === "repo-blob" ? node.origin.hash : null;
366
+ let size = 0;
367
+ if (node.state.content !== void 0) size = node.state.content.length;
368
+ else if (hash !== null) size = readRepoBlobContent(source, hash, path).length;
369
+ return {
370
+ kind: "blob",
371
+ mode: node.state.mode,
372
+ size,
373
+ hash
374
+ };
375
+ }
376
+ function statDirectoryNode(node) {
377
+ return {
378
+ kind: "tree",
379
+ mode: "40000",
380
+ size: 0,
381
+ hash: node.origin.kind === "repo-tree" ? node.origin.hash : null
382
+ };
383
+ }
384
+ function cloneNodeGraphForCopy(source, state, node, path) {
385
+ const newNodeId = createNodeId();
386
+ const cloned = cloneSessionNodeForCopy(node, newNodeId);
387
+ state.setNode(cloned);
388
+ if (node.state.kind !== "directory" || cloned.state.kind !== "directory") return newNodeId;
389
+ let overlay = cloned.state.overlay;
390
+ const children = listDirectoryChildren(source, state, node, path);
391
+ for (const child of children) {
392
+ const childNode = state.getNode(child.nodeId);
393
+ if (childNode === null) continue;
394
+ const clonedChildId = cloneNodeGraphForCopy(source, state, childNode, path === "" ? child.name : `${path}/${child.name}`);
395
+ overlay = overlayBindEntry(overlay, child.name, clonedChildId);
396
+ }
397
+ state.setNode({
398
+ ...cloned,
399
+ state: {
400
+ kind: "directory",
401
+ overlay
402
+ }
403
+ });
404
+ return newNodeId;
405
+ }
406
+ //#endregion
407
+ export { createVirtualWorkdirSession, openVirtualWorkdirSession };
@@ -0,0 +1,30 @@
1
+ import { VirtualWorkdirBackend } from "./core.mjs";
2
+ import { Database } from "bun:sqlite";
3
+
4
+ //#region src/workdir/sqlite-backend.d.ts
5
+ /** 创建 SQLite Virtual Workdir backend 的可选参数 */
6
+ interface CreateSqliteVirtualWorkdirBackendOptions {
7
+ /** 开启 WAL 模式,默认 true */
8
+ readonly walMode?: boolean;
9
+ }
10
+ /**
11
+ * SQLite Virtual Workdir backend(含资源释放能力)
12
+ */
13
+ interface SqliteVirtualWorkdirBackend extends VirtualWorkdirBackend {
14
+ /** 释放 SQLite 数据库连接 */
15
+ [Symbol.dispose](): void;
16
+ }
17
+ /**
18
+ * 创建基于 SQLite 的 Virtual Workdir backend
19
+ *
20
+ * @example
21
+ * ```ts
22
+ * using backend = createSqliteVirtualWorkdirBackend(":memory:");
23
+ * const sessionId = backend.createSession({ baseTree: tree });
24
+ * const session = backend.openSession(repo.objects, sessionId);
25
+ * expect(session.baseTree).toBe(tree);
26
+ * ```
27
+ */
28
+ declare function createSqliteVirtualWorkdirBackend(dbPath: string, options?: CreateSqliteVirtualWorkdirBackendOptions): SqliteVirtualWorkdirBackend;
29
+ //#endregion
30
+ export { CreateSqliteVirtualWorkdirBackendOptions, SqliteVirtualWorkdirBackend, createSqliteVirtualWorkdirBackend };