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.
Files changed (39) hide show
  1. package/README.md +6 -0
  2. package/dist/workdir/change-index-plan.mjs +76 -0
  3. package/dist/workdir/change-index.d.mts +21 -0
  4. package/dist/workdir/change-index.mjs +413 -0
  5. package/dist/workdir/core.d.mts +64 -63
  6. package/dist/workdir/directory-view.mjs +197 -0
  7. package/dist/workdir/dirty-dir-plan.mjs +73 -0
  8. package/dist/workdir/dirty-dir.d.mts +28 -0
  9. package/dist/workdir/dirty-dir.mjs +32 -0
  10. package/dist/workdir/file-backend.d.mts +34 -12
  11. package/dist/workdir/file-backend.mjs +161 -94
  12. package/dist/workdir/file.d.mts +2 -2
  13. package/dist/workdir/file.mjs +2 -2
  14. package/dist/workdir/ids.d.mts +1 -1
  15. package/dist/workdir/ids.mjs +3 -3
  16. package/dist/workdir/memory-backend.mjs +34 -50
  17. package/dist/workdir/memory.d.mts +2 -3
  18. package/dist/workdir/memory.mjs +2 -3
  19. package/dist/workdir/nodes.d.mts +7 -7
  20. package/dist/workdir/nodes.mjs +3 -9
  21. package/dist/workdir/overlay.d.mts +3 -3
  22. package/dist/workdir/sqlite-backend.d.mts +31 -14
  23. package/dist/workdir/sqlite-backend.mjs +238 -142
  24. package/dist/workdir/sqlite.d.mts +2 -2
  25. package/dist/workdir/sqlite.mjs +2 -2
  26. package/dist/workdir/state-store.d.mts +23 -12
  27. package/dist/workdir/workdir-path.mjs +348 -0
  28. package/dist/workdir/workdir-transaction.mjs +115 -0
  29. package/dist/workdir/workdir.d.mts +30 -0
  30. package/dist/workdir/workdir.mjs +280 -0
  31. package/dist/workdir/write-tree.mjs +108 -44
  32. package/package.json +2 -1
  33. package/dist/workdir/change-log.d.mts +0 -25
  34. package/dist/workdir/change-log.mjs +0 -69
  35. package/dist/workdir/memory-backend.d.mts +0 -17
  36. package/dist/workdir/session-id.mjs +0 -18
  37. package/dist/workdir/session-internal.mjs +0 -141
  38. package/dist/workdir/session.d.mts +0 -30
  39. package/dist/workdir/session.mjs +0 -407
@@ -0,0 +1,348 @@
1
+ import { VirtualNotDirectoryError, VirtualNotFileError, VirtualPathAlreadyExistsError, VirtualPathNotFoundError } from "../core/errors.mjs";
2
+ import { VIRTUAL_ROOT_NODE_ID, originBackedNodeId } from "./ids.mjs";
3
+ import { readRepoTree, treeEntryToNodeOrigin } from "./origin.mjs";
4
+ import { mergeDirectoryChildren } from "./overlay.mjs";
5
+ import { assertValidVirtualPath, baseName, joinPath, parentPath, splitPathSegments } from "./path.mjs";
6
+ //#region src/workdir/workdir-path.ts
7
+ /**
8
+ * Virtual Workdir 路径解析与共享读视图
9
+ *
10
+ * 供 workdir.ts 与 write-tree.ts 复用:
11
+ * - 路径解析(resolvePath / resolveWriteTarget*)
12
+ * - 目录子项展开(listDirectoryChildren / getDirectoryChildrenView)
13
+ * - origin 节点懒注册(ensureNodeFromTreeEntry)
14
+ *
15
+ * 目录观察与编译计划相关的类型和函数已拆分到 directory-view.ts。
16
+ */
17
+ /**
18
+ * 基于目录路径与子项名称拼出子路径。
19
+ *
20
+ * @example
21
+ * ```ts
22
+ * expect(joinChildPath("", "a.txt")).toBe("a.txt");
23
+ * expect(joinChildPath("src", "a.txt")).toBe("src/a.txt");
24
+ * ```
25
+ */
26
+ function joinChildPath(dirPath, name) {
27
+ return dirPath === "" ? name : `${dirPath}/${name}`;
28
+ }
29
+ function getRootNode(state) {
30
+ const root = state.getNode(VIRTUAL_ROOT_NODE_ID);
31
+ if (root === null) throw new Error("Virtual workdir is missing root node");
32
+ return root;
33
+ }
34
+ /**
35
+ * 从根目录沿路径分段解析到目标节点
36
+ */
37
+ function resolvePath(source, state, path) {
38
+ assertValidVirtualPath(path);
39
+ const segments = splitPathSegments(path);
40
+ let current = getRootNode(state);
41
+ let currentPath = "";
42
+ for (const segment of segments) {
43
+ if (current.state.kind !== "directory") return {
44
+ found: false,
45
+ node: null
46
+ };
47
+ const child = getDirectoryChildrenView(source, state, current, currentPath).get(segment);
48
+ if (child === void 0) return {
49
+ found: false,
50
+ node: null
51
+ };
52
+ currentPath = joinPath(currentPath === "" ? null : currentPath, segment);
53
+ const childNode = state.getNode(child.nodeId);
54
+ if (childNode === null) return {
55
+ found: false,
56
+ node: null
57
+ };
58
+ current = childNode;
59
+ }
60
+ return {
61
+ found: true,
62
+ node: current
63
+ };
64
+ }
65
+ /**
66
+ * 解析写路径的父目录,并保证其存在且为目录。
67
+ *
68
+ * @example
69
+ * ```ts
70
+ * const target = resolveWriteParentDirectory(source, state, "src/a.txt");
71
+ * expect(target.parentPath).toBe("src");
72
+ * expect(target.name).toBe("a.txt");
73
+ * ```
74
+ */
75
+ function resolveWriteParentDirectory(source, state, path) {
76
+ const target = resolvePathParentLookupContext(source, state, path);
77
+ if (target.parentNode === null) throw new VirtualPathNotFoundError(target.parentPath || path);
78
+ if (target.parentNode.state.kind !== "directory") throw new VirtualNotDirectoryError(target.parentPath || path);
79
+ return {
80
+ parentPath: target.parentPath,
81
+ name: target.name,
82
+ parentNode: target.parentNode
83
+ };
84
+ }
85
+ /**
86
+ * 解析写路径对应的父目录与当前可见目标子项。
87
+ *
88
+ * @example
89
+ * ```ts
90
+ * const target = resolveWriteTargetInParent(source, state, "src/a.txt");
91
+ * expect(target.parentPath).toBe("src");
92
+ * ```
93
+ */
94
+ function resolveWriteTargetInParent(source, state, path) {
95
+ const parent = resolveWriteParentDirectory(source, state, path);
96
+ const existing = resolveDirectoryChild(source, state, parent.parentNode, parent.parentPath, parent.name);
97
+ return {
98
+ ...parent,
99
+ existing: existing.found ? existing : null
100
+ };
101
+ }
102
+ /**
103
+ * 解析写路径,并要求当前目标已存在。
104
+ *
105
+ * @example
106
+ * ```ts
107
+ * const target = requireExistingWriteTarget(source, state, "a.txt");
108
+ * expect(target.existing.child.name).toBe("a.txt");
109
+ * ```
110
+ */
111
+ function requireExistingWriteTarget(source, state, path) {
112
+ const target = resolveWriteTargetInParent(source, state, path);
113
+ if (target.existing === null) throw new VirtualPathNotFoundError(path);
114
+ return {
115
+ parentPath: target.parentPath,
116
+ name: target.name,
117
+ parentNode: target.parentNode,
118
+ existing: target.existing
119
+ };
120
+ }
121
+ /**
122
+ * 解析写路径,并要求目标当前不存在。
123
+ *
124
+ * @example
125
+ * ```ts
126
+ * const target = requireMissingWriteTarget(source, state, "a.txt");
127
+ * expect(target.name).toBe("a.txt");
128
+ * ```
129
+ */
130
+ function requireMissingWriteTarget(source, state, path) {
131
+ const target = resolveWriteTargetInParent(source, state, path);
132
+ if (target.existing !== null) throw new VirtualPathAlreadyExistsError(path);
133
+ return {
134
+ parentPath: target.parentPath,
135
+ name: target.name,
136
+ parentNode: target.parentNode
137
+ };
138
+ }
139
+ /**
140
+ * 解析允许写入 blob/symlink 的目标。
141
+ *
142
+ * 若目标已存在且为目录,则抛出 `VirtualNotFileError`。
143
+ *
144
+ * @example
145
+ * ```ts
146
+ * const target = resolveLeafWriteTarget(source, state, "a.txt");
147
+ * expect(target.name).toBe("a.txt");
148
+ * ```
149
+ */
150
+ function resolveLeafWriteTarget(source, state, path) {
151
+ const target = resolveWriteTargetInParent(source, state, path);
152
+ if (target.existing !== null && target.existing.node.state.kind === "directory") throw new VirtualNotFileError(path);
153
+ return {
154
+ parentPath: target.parentPath,
155
+ name: target.name,
156
+ parentNode: target.parentNode,
157
+ existing: target.existing === null ? null : {
158
+ child: target.existing.child,
159
+ node: target.existing.node
160
+ }
161
+ };
162
+ }
163
+ /**
164
+ * 按路径定向解析当前叶子节点。
165
+ *
166
+ * 目录或不存在的路径都会返回 `null`。
167
+ *
168
+ * @example
169
+ * ```ts
170
+ * const leaf = resolveCurrentLeafAtPath(source, state, "a.txt");
171
+ * expect(leaf?.node.state.kind).toBe("file");
172
+ * ```
173
+ */
174
+ function resolveCurrentLeafAtPath(source, state, path) {
175
+ const resolved = resolvePathByParentLookup(source, state, path);
176
+ if (!resolved.found || resolved.node === null || resolved.node.state.kind === "directory") return null;
177
+ return {
178
+ path,
179
+ node: resolved.node
180
+ };
181
+ }
182
+ /**
183
+ * 解析 `rename` / `copy` 这类“已存在源 -> 目标路径”的双路径上下文。
184
+ *
185
+ * @example
186
+ * ```ts
187
+ * const transfer = resolveWriteTransfer(source, state, "a.txt", "b.txt");
188
+ * expect(transfer.from.existing.child.name).toBe("a.txt");
189
+ * expect(transfer.to.name).toBe("b.txt");
190
+ * ```
191
+ */
192
+ function resolveWriteTransfer(source, state, from, to) {
193
+ assertValidVirtualPath(from);
194
+ assertValidVirtualPath(to);
195
+ return {
196
+ from: requireExistingWriteTarget(source, state, from),
197
+ to: resolveWriteTargetInParent(source, state, to)
198
+ };
199
+ }
200
+ /**
201
+ * 根据父目录与子项名称定向解析当前可见子项及其节点。
202
+ */
203
+ function resolveDirectoryChild(source, state, parentNode, parentPath, name) {
204
+ if (parentNode.state.kind !== "directory") return {
205
+ found: false,
206
+ child: null,
207
+ node: null
208
+ };
209
+ const child = getDirectoryChildrenView(source, state, parentNode, parentPath).get(name);
210
+ if (child === void 0) return {
211
+ found: false,
212
+ child: null,
213
+ node: null
214
+ };
215
+ const childNode = state.getNode(child.nodeId);
216
+ if (childNode === null) return {
217
+ found: false,
218
+ child: null,
219
+ node: null
220
+ };
221
+ return {
222
+ found: true,
223
+ child,
224
+ node: childNode
225
+ };
226
+ }
227
+ /**
228
+ * 获取目录当前视图,包含排序后的完整子项与按名查询能力。
229
+ *
230
+ * @example
231
+ * ```ts
232
+ * const view = getDirectoryChildrenView(source, state, dirNode, "");
233
+ * expect(view.children.length).toBeGreaterThanOrEqual(0);
234
+ * ```
235
+ */
236
+ function getDirectoryChildrenView(source, state, dirNode, dirPath) {
237
+ const children = listDirectoryChildren(source, state, dirNode, dirPath);
238
+ const childrenByName = new Map(children.map((child) => [child.name, child]));
239
+ return {
240
+ children,
241
+ get(name) {
242
+ return childrenByName.get(name);
243
+ }
244
+ };
245
+ }
246
+ /**
247
+ * 以“先父目录、后最后一段”的方式解析路径。
248
+ *
249
+ * 适用于只关心最终条目的场景,便于复用目录级定向解析 helper。
250
+ */
251
+ function resolvePathByParentLookup(source, state, path) {
252
+ const target = resolvePathParentLookupContext(source, state, path);
253
+ if (target.parentNode === null) return {
254
+ found: false,
255
+ node: null
256
+ };
257
+ const resolved = resolveDirectoryChild(source, state, target.parentNode, target.parentPath, target.name);
258
+ if (!resolved.found) return {
259
+ found: false,
260
+ node: null
261
+ };
262
+ return {
263
+ found: true,
264
+ node: resolved.node
265
+ };
266
+ }
267
+ /**
268
+ * 确保 origin 条目对应的 workdir 节点已懒注册
269
+ */
270
+ function ensureNodeFromTreeEntry(state, entry) {
271
+ const id = originBackedNodeId(entry.hash);
272
+ if (state.getNode(id) === null) {
273
+ const origin = treeEntryToNodeOrigin(entry);
274
+ let nodeState;
275
+ if (entry.mode === "40000") nodeState = {
276
+ kind: "directory",
277
+ overlay: {
278
+ addedEntries: /* @__PURE__ */ new Map(),
279
+ deletedNames: /* @__PURE__ */ new Set()
280
+ }
281
+ };
282
+ else if (entry.mode === "120000") nodeState = {
283
+ kind: "symlink",
284
+ mode: "120000"
285
+ };
286
+ else nodeState = {
287
+ kind: "file",
288
+ mode: entry.mode === "100755" ? "100755" : "100644"
289
+ };
290
+ state.setNode({
291
+ id,
292
+ origin,
293
+ state: nodeState
294
+ });
295
+ }
296
+ return id;
297
+ }
298
+ /**
299
+ * 展开目录的完整子项列表(origin + overlay 合成)
300
+ */
301
+ function listDirectoryChildren(source, state, dirNode, dirPath) {
302
+ if (dirNode.state.kind !== "directory") throw new VirtualNotDirectoryError(dirPath);
303
+ let originChildren = [];
304
+ if (dirNode.origin.kind === "repo-tree") originChildren = readRepoTree(source, dirNode.origin.hash, dirPath).entries.map((entry) => {
305
+ const childNodeId = ensureNodeFromTreeEntry(state, entry);
306
+ return {
307
+ name: entry.name,
308
+ mode: entry.mode,
309
+ nodeId: childNodeId
310
+ };
311
+ });
312
+ const addedModes = buildAddedModes(state, dirNode);
313
+ return mergeDirectoryChildren(originChildren, dirNode.state.overlay, addedModes);
314
+ }
315
+ /**
316
+ * 构建 overlay addedEntries 对应的 mode map
317
+ */
318
+ function buildAddedModes(state, dirNode) {
319
+ if (dirNode.state.kind !== "directory") return /* @__PURE__ */ new Map();
320
+ const addedModes = /* @__PURE__ */ new Map();
321
+ for (const name of dirNode.state.overlay.addedEntries.keys()) {
322
+ const nodeId = dirNode.state.overlay.addedEntries.get(name);
323
+ const node = state.getNode(nodeId);
324
+ if (node !== null) addedModes.set(name, workdirNodeMode(node));
325
+ }
326
+ return addedModes;
327
+ }
328
+ function workdirNodeMode(node) {
329
+ if (node.state.kind === "directory") return "40000";
330
+ if (node.state.kind === "symlink") return "120000";
331
+ return node.state.mode;
332
+ }
333
+ function resolvePathParentLookupContext(source, state, path) {
334
+ assertValidVirtualPath(path);
335
+ const parent = parentPath(path);
336
+ const name = baseName(path);
337
+ const parentResolved = parent !== null ? resolvePath(source, state, parent) : {
338
+ found: true,
339
+ node: getRootNode(state)
340
+ };
341
+ return {
342
+ parentPath: parent ?? "",
343
+ name,
344
+ parentNode: parentResolved.found ? parentResolved.node : null
345
+ };
346
+ }
347
+ //#endregion
348
+ export { ensureNodeFromTreeEntry, getDirectoryChildrenView, getRootNode, joinChildPath, listDirectoryChildren, requireExistingWriteTarget, requireMissingWriteTarget, resolveCurrentLeafAtPath, resolveLeafWriteTarget, resolvePath, resolveWriteTransfer };
@@ -0,0 +1,115 @@
1
+ import { createNodeId } from "./ids.mjs";
2
+ import { readRepoBlobContent } from "./origin.mjs";
3
+ import { overlayBindEntry } from "./overlay.mjs";
4
+ import { listDirectoryChildren } from "./workdir-path.mjs";
5
+ import { observeListedDirectoryChild } from "./directory-view.mjs";
6
+ import { cloneWorkdirNodeForCopy } from "./nodes.mjs";
7
+ //#region src/workdir/workdir-transaction.ts
8
+ /**
9
+ * Virtual Workdir 写事务与节点辅助函数
10
+ *
11
+ * 从 workdir.ts 提取,降低编排层复杂度:
12
+ * - 写操作事务生命周期管理
13
+ * - 父目录 overlay 更新
14
+ * - 节点状态统计(stat)
15
+ * - 递归节点图克隆(copy)
16
+ */
17
+ /**
18
+ * 在 state store 事务边界内执行写操作。
19
+ *
20
+ * @param state - workdir 内部状态存储
21
+ * @param onBeforeCommit - 提交前回调(在事务 callback 内执行);可为 null
22
+ * @param onCommitted - 提交后回调(在事务 callback 外执行)
23
+ * @param fn - 实际写入逻辑
24
+ */
25
+ function runInWriteTransaction(state, onBeforeCommit, onCommitted, fn) {
26
+ const result = onBeforeCommit === null ? state.transact(fn) : state.transact(() => {
27
+ const innerResult = fn();
28
+ onBeforeCommit();
29
+ return innerResult;
30
+ });
31
+ onCommitted();
32
+ return result;
33
+ }
34
+ /**
35
+ * 更新父节点的 overlay(创建新节点对象替代 Map 中的旧引用)
36
+ */
37
+ function updateParentOverlay(state, parentId, newOverlay) {
38
+ const parentNode = state.getNode(parentId);
39
+ if (parentNode === null || parentNode.state.kind !== "directory") throw new Error("updateParentOverlay: parent is not a directory");
40
+ state.setNode({
41
+ ...parentNode,
42
+ state: {
43
+ ...parentNode.state,
44
+ overlay: newOverlay
45
+ }
46
+ });
47
+ }
48
+ /**
49
+ * 获取节点统计信息(用于 workdir.stat() 实现)
50
+ */
51
+ function statNode(source, node, path) {
52
+ if (node.state.kind === "directory") return statDirectoryNode(node);
53
+ if (node.state.kind === "symlink") {
54
+ const hash = node.origin.kind === "repo-blob" ? node.origin.hash : null;
55
+ let size = 0;
56
+ if (node.state.target !== void 0) size = node.state.target.length;
57
+ else if (hash !== null) size = readRepoBlobContent(source, hash, path).length;
58
+ return {
59
+ kind: "symlink",
60
+ mode: "120000",
61
+ size,
62
+ hash
63
+ };
64
+ }
65
+ const hash = node.origin.kind === "repo-blob" ? node.origin.hash : null;
66
+ let size = 0;
67
+ if (node.state.content !== void 0) size = node.state.content.length;
68
+ else if (hash !== null) size = readRepoBlobContent(source, hash, path).length;
69
+ return {
70
+ kind: "blob",
71
+ mode: node.state.mode,
72
+ size,
73
+ hash
74
+ };
75
+ }
76
+ /**
77
+ * 目录节点统计信息(无大小,hash 取 origin)
78
+ */
79
+ function statDirectoryNode(node) {
80
+ return {
81
+ kind: "tree",
82
+ mode: "40000",
83
+ size: 0,
84
+ hash: node.origin.kind === "repo-tree" ? node.origin.hash : null
85
+ };
86
+ }
87
+ /**
88
+ * 递归克隆节点图(用于 copy 语义)。
89
+ *
90
+ * 为新节点分配新身份,避免 copy 后源与目标共享子节点身份。
91
+ */
92
+ function cloneNodeGraphForCopy(source, state, node, path) {
93
+ const newNodeId = createNodeId();
94
+ const cloned = cloneWorkdirNodeForCopy(node, newNodeId);
95
+ state.setNode(cloned);
96
+ if (node.state.kind !== "directory" || cloned.state.kind !== "directory") return newNodeId;
97
+ let overlay = cloned.state.overlay;
98
+ const children = listDirectoryChildren(source, state, node, path);
99
+ for (const child of children) {
100
+ const observedChild = observeListedDirectoryChild(state, path, child);
101
+ if (observedChild === null) continue;
102
+ const clonedChildId = cloneNodeGraphForCopy(source, state, observedChild.node, observedChild.path);
103
+ overlay = overlayBindEntry(overlay, observedChild.name, clonedChildId);
104
+ }
105
+ state.setNode({
106
+ ...cloned,
107
+ state: {
108
+ kind: "directory",
109
+ overlay
110
+ }
111
+ });
112
+ return newNodeId;
113
+ }
114
+ //#endregion
115
+ export { cloneNodeGraphForCopy, runInWriteTransaction, statDirectoryNode, statNode, updateParentOverlay };
@@ -0,0 +1,30 @@
1
+ import { ObjectDatabase } from "../core/types/odb.mjs";
2
+ import { CreateVirtualWorkdirOptions, VirtualWorkdir } from "./core.mjs";
3
+ import { VirtualWorkdirStateStore } from "./state-store.mjs";
4
+
5
+ //#region src/workdir/workdir.d.ts
6
+ /**
7
+ * 基于 ObjectDatabase 创建 VirtualWorkdir
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * const repo = createMemoryRepository();
12
+ * const tree = repo.createTree([]);
13
+ * const workdir = createVirtualWorkdir(repo.objects, { baseTree: tree });
14
+ * expect(workdir.readdir()).toEqual([]);
15
+ * ```
16
+ */
17
+ declare function createVirtualWorkdir(source: ObjectDatabase, options: CreateVirtualWorkdirOptions): VirtualWorkdir;
18
+ /**
19
+ * 基于已有状态存储打开 VirtualWorkdir
20
+ *
21
+ * @example
22
+ * ```ts
23
+ * const store = createVirtualWorkdirMemoryStateStore(tree);
24
+ * const workdir = openVirtualWorkdir(repo.objects, store);
25
+ * expect(workdir.baseTree).toBe(tree);
26
+ * ```
27
+ */
28
+ declare function openVirtualWorkdir(source: ObjectDatabase, state: VirtualWorkdirStateStore): VirtualWorkdir;
29
+ //#endregion
30
+ export { createVirtualWorkdir, openVirtualWorkdir };