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
|
@@ -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 };
|