nano-git 0.2.3 → 0.3.1

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.
@@ -1,17 +1,31 @@
1
- import { VirtualNotDirectoryError } from "../core/errors.mjs";
1
+ import { VirtualNotDirectoryError, VirtualNotFileError, VirtualPathAlreadyExistsError, VirtualPathNotFoundError } from "../core/errors.mjs";
2
2
  import { VIRTUAL_ROOT_NODE_ID, originBackedNodeId } from "./ids.mjs";
3
3
  import { mergeDirectoryChildren } from "./overlay.mjs";
4
4
  import { readRepoTree, treeEntryToNodeOrigin } from "./origin.mjs";
5
- import { assertValidVirtualPath, joinPath, splitPathSegments } from "./path.mjs";
5
+ import { assertValidVirtualPath, baseName, joinPath, parentPath, splitPathSegments } from "./path.mjs";
6
6
  //#region src/workdir/session-internal.ts
7
7
  /**
8
8
  * Virtual Workdir session 内部共享逻辑
9
9
  *
10
10
  * 供 session.ts 与 write-tree.ts 复用:
11
- * - 路径解析(resolvePath
12
- * - 目录子项展开(listDirectoryChildren)
11
+ * - 路径解析(resolvePath / resolveWriteTarget*)
12
+ * - 目录子项展开(listDirectoryChildren / getDirectoryChildrenView
13
13
  * - origin 节点懒注册(ensureNodeFromTreeEntry)
14
+ *
15
+ * 目录观察与编译计划相关的类型和函数已拆分到 directory-view.ts。
14
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
+ }
15
29
  function getRootNode(state) {
16
30
  const root = state.getNode(VIRTUAL_ROOT_NODE_ID);
17
31
  if (root === null) throw new Error("Virtual workdir session is missing root node");
@@ -30,7 +44,7 @@ function resolvePath(source, state, path) {
30
44
  found: false,
31
45
  node: null
32
46
  };
33
- const child = listDirectoryChildren(source, state, current, currentPath).find((c) => c.name === segment);
47
+ const child = getDirectoryChildrenView(source, state, current, currentPath).get(segment);
34
48
  if (child === void 0) return {
35
49
  found: false,
36
50
  node: null
@@ -49,29 +63,208 @@ function resolvePath(source, state, path) {
49
63
  };
50
64
  }
51
65
  /**
52
- * 根据父节点与子条目名解析子节点
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
+ * ```
53
191
  */
54
- function resolveChild(source, state, parentNode, parentPath, name) {
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) {
55
204
  if (parentNode.state.kind !== "directory") return {
56
205
  found: false,
206
+ child: null,
57
207
  node: null
58
208
  };
59
- const child = listDirectoryChildren(source, state, parentNode, parentPath).find((c) => c.name === name);
209
+ const child = getDirectoryChildrenView(source, state, parentNode, parentPath).get(name);
60
210
  if (child === void 0) return {
61
211
  found: false,
212
+ child: null,
62
213
  node: null
63
214
  };
64
215
  const childNode = state.getNode(child.nodeId);
65
216
  if (childNode === null) return {
66
217
  found: false,
218
+ child: null,
67
219
  node: null
68
220
  };
69
221
  return {
70
222
  found: true,
223
+ child,
71
224
  node: childNode
72
225
  };
73
226
  }
74
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
+ /**
75
268
  * 确保 origin 条目对应的 session 节点已懒注册
76
269
  */
77
270
  function ensureNodeFromTreeEntry(state, entry) {
@@ -137,5 +330,19 @@ function sessionNodeMode(node) {
137
330
  if (node.state.kind === "symlink") return "120000";
138
331
  return node.state.mode;
139
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
+ }
140
347
  //#endregion
141
- export { listDirectoryChildren, resolveChild, resolvePath };
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 { overlayBindEntry } from "./overlay.mjs";
3
+ import { cloneSessionNodeForCopy } from "./nodes.mjs";
4
+ import { readRepoBlobContent } from "./origin.mjs";
5
+ import { listDirectoryChildren } from "./session-internal.mjs";
6
+ import { observeListedDirectoryChild } from "./directory-view.mjs";
7
+ //#region src/workdir/session-transaction.ts
8
+ /**
9
+ * Virtual Workdir 事务包装与会话辅助函数
10
+ *
11
+ * 从 session.ts 提取,降低编排层的复杂度:
12
+ * - 写操作事务生命周期管理
13
+ * - 父目录 overlay 更新
14
+ * - 节点状态统计(stat)
15
+ * - 递归节点图克隆(copy)
16
+ */
17
+ /**
18
+ * 在 state store 事务边界内执行写操作。
19
+ *
20
+ * @param state - session 内部状态存储
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
+ * 获取节点统计信息(用于 session.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 = cloneSessionNodeForCopy(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 };