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.
- package/dist/core/errors.d.mts +71 -1
- package/dist/core/errors.mjs +99 -1
- package/dist/errors.d.mts +2 -2
- package/dist/errors.mjs +2 -2
- package/dist/log/index.d.mts +3 -0
- package/dist/log/index.mjs +2 -0
- package/dist/log/types.d.mts +71 -0
- package/dist/log/walk.d.mts +37 -0
- package/dist/log/walk.mjs +274 -0
- package/dist/workdir/change-log.d.mts +25 -0
- package/dist/workdir/change-log.mjs +69 -0
- package/dist/workdir/core.d.mts +188 -0
- package/dist/workdir/core.mjs +2 -0
- package/dist/workdir/file-backend.d.mts +22 -0
- package/dist/workdir/file-backend.mjs +366 -0
- package/dist/workdir/file.d.mts +2 -0
- package/dist/workdir/file.mjs +2 -0
- package/dist/workdir/ids.d.mts +7 -0
- package/dist/workdir/ids.mjs +20 -0
- package/dist/workdir/memory-backend.d.mts +17 -0
- package/dist/workdir/memory-backend.mjs +159 -0
- package/dist/workdir/memory.d.mts +3 -0
- package/dist/workdir/memory.mjs +3 -0
- package/dist/workdir/nodes.d.mts +57 -0
- package/dist/workdir/nodes.mjs +112 -0
- package/dist/workdir/origin.mjs +55 -0
- package/dist/workdir/overlay.d.mts +14 -0
- package/dist/workdir/overlay.mjs +95 -0
- package/dist/workdir/path.mjs +70 -0
- package/dist/workdir/session-id.mjs +18 -0
- package/dist/workdir/session-internal.mjs +141 -0
- package/dist/workdir/session.d.mts +30 -0
- package/dist/workdir/session.mjs +407 -0
- package/dist/workdir/sqlite-backend.d.mts +30 -0
- package/dist/workdir/sqlite-backend.mjs +363 -0
- package/dist/workdir/sqlite.d.mts +2 -0
- package/dist/workdir/sqlite.mjs +2 -0
- package/dist/workdir/state-store.d.mts +44 -0
- package/dist/workdir/write-tree.mjs +97 -0
- package/package.json +13 -3
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { createVirtualChangeLog } from "./change-log.mjs";
|
|
2
|
+
import { VIRTUAL_ROOT_NODE_ID } from "./ids.mjs";
|
|
3
|
+
import { createRootDirectoryNode } from "./nodes.mjs";
|
|
4
|
+
import { createVirtualWorkdirSessionId } from "./session-id.mjs";
|
|
5
|
+
import { openVirtualWorkdirSession } from "./session.mjs";
|
|
6
|
+
//#region src/workdir/memory-backend.ts
|
|
7
|
+
/**
|
|
8
|
+
* Virtual Workdir 内存状态存储
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* 创建内存版状态存储
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* const store = createVirtualWorkdirMemoryStateStore(tree);
|
|
16
|
+
* expect(store.readBaseTree()).toBe(tree);
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
function createVirtualWorkdirMemoryStateStore(baseTree) {
|
|
20
|
+
const state = {
|
|
21
|
+
baseTree,
|
|
22
|
+
nodes: /* @__PURE__ */ new Map(),
|
|
23
|
+
changeLog: createVirtualChangeLog()
|
|
24
|
+
};
|
|
25
|
+
resetState(state, baseTree);
|
|
26
|
+
return {
|
|
27
|
+
kind: "memory",
|
|
28
|
+
transact(fn) {
|
|
29
|
+
const snapshot = snapshotState(state);
|
|
30
|
+
try {
|
|
31
|
+
return fn();
|
|
32
|
+
} catch (error) {
|
|
33
|
+
restoreState(state, snapshot);
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
readBaseTree() {
|
|
38
|
+
return state.baseTree;
|
|
39
|
+
},
|
|
40
|
+
writeBaseTree(nextBaseTree) {
|
|
41
|
+
state.baseTree = nextBaseTree;
|
|
42
|
+
},
|
|
43
|
+
getNode(id) {
|
|
44
|
+
return state.nodes.get(id) ?? null;
|
|
45
|
+
},
|
|
46
|
+
setNode(node) {
|
|
47
|
+
state.nodes.set(node.id, node);
|
|
48
|
+
},
|
|
49
|
+
deleteNode(id) {
|
|
50
|
+
state.nodes.delete(id);
|
|
51
|
+
},
|
|
52
|
+
appendChange(record) {
|
|
53
|
+
state.changeLog.append(record);
|
|
54
|
+
},
|
|
55
|
+
listChangeRecords() {
|
|
56
|
+
return state.changeLog.snapshot();
|
|
57
|
+
},
|
|
58
|
+
clearChanges() {
|
|
59
|
+
state.changeLog.clear();
|
|
60
|
+
},
|
|
61
|
+
reset(nextBaseTree) {
|
|
62
|
+
resetState(state, nextBaseTree);
|
|
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;
|
|
85
|
+
},
|
|
86
|
+
openSession(source, sessionId) {
|
|
87
|
+
const store = sessions.get(sessionId);
|
|
88
|
+
if (store === void 0) throw new Error(`Virtual workdir session not found: ${sessionId}`);
|
|
89
|
+
return openVirtualWorkdirSession(source, store);
|
|
90
|
+
},
|
|
91
|
+
deleteSession(sessionId) {
|
|
92
|
+
if (!sessions.delete(sessionId)) throw new Error(`Virtual workdir session not found: ${sessionId}`);
|
|
93
|
+
},
|
|
94
|
+
listSessions() {
|
|
95
|
+
return Array.from(sessions.keys());
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function resetState(state, baseTree) {
|
|
100
|
+
state.baseTree = baseTree;
|
|
101
|
+
state.nodes.clear();
|
|
102
|
+
state.nodes.set(VIRTUAL_ROOT_NODE_ID, createRootDirectoryNode(baseTree));
|
|
103
|
+
state.changeLog.clear();
|
|
104
|
+
}
|
|
105
|
+
function snapshotState(state) {
|
|
106
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
107
|
+
for (const [nodeId, node] of state.nodes) nodes.set(nodeId, cloneSessionNode(node));
|
|
108
|
+
return {
|
|
109
|
+
baseTree: state.baseTree,
|
|
110
|
+
nodes,
|
|
111
|
+
changes: state.changeLog.snapshot()
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function restoreState(state, snapshot) {
|
|
115
|
+
state.baseTree = snapshot.baseTree;
|
|
116
|
+
state.nodes.clear();
|
|
117
|
+
for (const [nodeId, node] of snapshot.nodes) state.nodes.set(nodeId, cloneSessionNode(node));
|
|
118
|
+
state.changeLog.clear();
|
|
119
|
+
for (const record of snapshot.changes) state.changeLog.append(record);
|
|
120
|
+
}
|
|
121
|
+
function cloneSessionNode(node) {
|
|
122
|
+
if (node.state.kind === "directory") return {
|
|
123
|
+
id: node.id,
|
|
124
|
+
origin: node.origin,
|
|
125
|
+
state: {
|
|
126
|
+
kind: "directory",
|
|
127
|
+
overlay: {
|
|
128
|
+
addedEntries: new Map(node.state.overlay.addedEntries),
|
|
129
|
+
deletedNames: new Set(node.state.overlay.deletedNames)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
if (node.state.kind === "file") return {
|
|
134
|
+
id: node.id,
|
|
135
|
+
origin: node.origin,
|
|
136
|
+
state: node.state.content === void 0 ? {
|
|
137
|
+
kind: "file",
|
|
138
|
+
mode: node.state.mode
|
|
139
|
+
} : {
|
|
140
|
+
kind: "file",
|
|
141
|
+
mode: node.state.mode,
|
|
142
|
+
content: Buffer.from(node.state.content)
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
return {
|
|
146
|
+
id: node.id,
|
|
147
|
+
origin: node.origin,
|
|
148
|
+
state: node.state.target === void 0 ? {
|
|
149
|
+
kind: "symlink",
|
|
150
|
+
mode: "120000"
|
|
151
|
+
} : {
|
|
152
|
+
kind: "symlink",
|
|
153
|
+
mode: "120000",
|
|
154
|
+
target: Buffer.from(node.state.target)
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
//#endregion
|
|
159
|
+
export { createMemoryVirtualWorkdirBackend, createVirtualWorkdirMemoryStateStore };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { SHA1 } from "../core/types.mjs";
|
|
2
|
+
import { NodeId } from "./ids.mjs";
|
|
3
|
+
import { DirectoryOverlay } from "./overlay.mjs";
|
|
4
|
+
|
|
5
|
+
//#region src/workdir/nodes.d.ts
|
|
6
|
+
/** Blob / 符号链接在 origin 与 state 中使用的 mode */
|
|
7
|
+
type BlobObjectMode = "100644" | "100755" | "120000";
|
|
8
|
+
/**
|
|
9
|
+
* 节点来源(repo 对象或纯 session 新建)
|
|
10
|
+
*/
|
|
11
|
+
type NodeOrigin = {
|
|
12
|
+
readonly kind: "none";
|
|
13
|
+
} | {
|
|
14
|
+
readonly kind: "repo-tree";
|
|
15
|
+
readonly hash: SHA1;
|
|
16
|
+
} | {
|
|
17
|
+
readonly kind: "repo-blob";
|
|
18
|
+
readonly mode: BlobObjectMode;
|
|
19
|
+
readonly hash: SHA1;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* 目录节点当前状态
|
|
23
|
+
*
|
|
24
|
+
* `overlay` 表达 session 层增删改;子项 nodeId 通过 overlay 合成与懒加载解析。
|
|
25
|
+
*/
|
|
26
|
+
interface DirectoryNodeState {
|
|
27
|
+
readonly kind: "directory";
|
|
28
|
+
readonly overlay: DirectoryOverlay;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* 文件节点当前状态(raw content overlay)
|
|
32
|
+
*/
|
|
33
|
+
interface FileNodeState {
|
|
34
|
+
readonly kind: "file";
|
|
35
|
+
readonly mode: "100644" | "100755";
|
|
36
|
+
/** 未设置时表示未 materialize,可读 origin */
|
|
37
|
+
readonly content?: Buffer;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* 符号链接节点当前状态
|
|
41
|
+
*/
|
|
42
|
+
interface SymlinkNodeState {
|
|
43
|
+
readonly kind: "symlink";
|
|
44
|
+
readonly mode: "120000";
|
|
45
|
+
readonly target?: Buffer;
|
|
46
|
+
}
|
|
47
|
+
type SessionNodeState = DirectoryNodeState | FileNodeState | SymlinkNodeState;
|
|
48
|
+
/**
|
|
49
|
+
* 完整的会话节点记录
|
|
50
|
+
*/
|
|
51
|
+
interface SessionNode {
|
|
52
|
+
readonly id: NodeId;
|
|
53
|
+
readonly origin: NodeOrigin;
|
|
54
|
+
readonly state: SessionNodeState;
|
|
55
|
+
}
|
|
56
|
+
//#endregion
|
|
57
|
+
export { SessionNode };
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { VIRTUAL_ROOT_NODE_ID } from "./ids.mjs";
|
|
2
|
+
import { cloneDirectoryOverlay, createEmptyDirectoryOverlay } from "./overlay.mjs";
|
|
3
|
+
//#region src/workdir/nodes.ts
|
|
4
|
+
/**
|
|
5
|
+
* Virtual Workdir 会话节点状态模型
|
|
6
|
+
*
|
|
7
|
+
* 节点身份(nodeId)与目录路径绑定分离;origin 描述 repo-backed 来源。
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* 创建绑定 repo 根 tree 的目录节点(带空 overlay)
|
|
11
|
+
*/
|
|
12
|
+
function createRootDirectoryNode(originTreeHash) {
|
|
13
|
+
return {
|
|
14
|
+
id: VIRTUAL_ROOT_NODE_ID,
|
|
15
|
+
origin: {
|
|
16
|
+
kind: "repo-tree",
|
|
17
|
+
hash: originTreeHash
|
|
18
|
+
},
|
|
19
|
+
state: {
|
|
20
|
+
kind: "directory",
|
|
21
|
+
overlay: createEmptyDirectoryOverlay()
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* 节点是否携带可 revert 的 repo origin
|
|
27
|
+
*/
|
|
28
|
+
function nodeHasRepoOrigin(node) {
|
|
29
|
+
return node.origin.kind === "repo-tree" || node.origin.kind === "repo-blob";
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* 目录 overlay 是否有 session 层修改
|
|
33
|
+
*/
|
|
34
|
+
function isDirectoryOverlayDirty(overlay) {
|
|
35
|
+
return overlay.addedEntries.size > 0 || overlay.deletedNames.size > 0;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* 将节点状态恢复到 origin 语义(丢弃 materialize / 目录 overlay)
|
|
39
|
+
*
|
|
40
|
+
* 无 repo origin 时返回原状态引用,由上层决定是否抛错。
|
|
41
|
+
*/
|
|
42
|
+
function revertNodeState(node) {
|
|
43
|
+
if (!nodeHasRepoOrigin(node)) return node;
|
|
44
|
+
if (node.state.kind === "directory") return {
|
|
45
|
+
...node,
|
|
46
|
+
state: {
|
|
47
|
+
kind: "directory",
|
|
48
|
+
overlay: createEmptyDirectoryOverlay()
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
if (node.state.kind === "file") {
|
|
52
|
+
const mode = node.state.mode;
|
|
53
|
+
return {
|
|
54
|
+
...node,
|
|
55
|
+
state: {
|
|
56
|
+
kind: "file",
|
|
57
|
+
mode
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
...node,
|
|
63
|
+
state: {
|
|
64
|
+
kind: "symlink",
|
|
65
|
+
mode: "120000"
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* 为 `copy` 创建新节点:共享 origin,目录为浅复制(子项绑定保留,但 nodeId 为新)
|
|
71
|
+
*/
|
|
72
|
+
function cloneSessionNodeForCopy(source, newId) {
|
|
73
|
+
const origin = source.origin;
|
|
74
|
+
if (source.state.kind === "directory") return {
|
|
75
|
+
id: newId,
|
|
76
|
+
origin,
|
|
77
|
+
state: {
|
|
78
|
+
kind: "directory",
|
|
79
|
+
overlay: cloneDirectoryOverlay(source.state.overlay)
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
if (source.state.kind === "file") {
|
|
83
|
+
const content = source.state.content;
|
|
84
|
+
return {
|
|
85
|
+
id: newId,
|
|
86
|
+
origin,
|
|
87
|
+
state: content === void 0 ? {
|
|
88
|
+
kind: "file",
|
|
89
|
+
mode: source.state.mode
|
|
90
|
+
} : {
|
|
91
|
+
kind: "file",
|
|
92
|
+
mode: source.state.mode,
|
|
93
|
+
content: Buffer.from(content)
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
const target = source.state.target;
|
|
98
|
+
return {
|
|
99
|
+
id: newId,
|
|
100
|
+
origin,
|
|
101
|
+
state: target === void 0 ? {
|
|
102
|
+
kind: "symlink",
|
|
103
|
+
mode: "120000"
|
|
104
|
+
} : {
|
|
105
|
+
kind: "symlink",
|
|
106
|
+
mode: "120000",
|
|
107
|
+
target: Buffer.from(target)
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
//#endregion
|
|
112
|
+
export { cloneSessionNodeForCopy, createRootDirectoryNode, isDirectoryOverlayDirty, revertNodeState };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { VirtualOriginUnavailableError } from "../core/errors.mjs";
|
|
2
|
+
import { tryReadObject } from "../objects/raw.mjs";
|
|
3
|
+
//#region src/workdir/origin.ts
|
|
4
|
+
/**
|
|
5
|
+
* 从对象源读取 repo-backed origin(tree/blob)
|
|
6
|
+
*
|
|
7
|
+
* 仅依赖 ObjectSource,不绑定具体 ODB 后端实现。
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* 读取 tree 对象;缺失时抛 VirtualOriginUnavailableError
|
|
11
|
+
*/
|
|
12
|
+
function readRepoTree(source, hash, path) {
|
|
13
|
+
const obj = tryReadObject(source, hash);
|
|
14
|
+
if (obj === void 0) throw new VirtualOriginUnavailableError(path, `Origin tree object missing: ${hash}`);
|
|
15
|
+
if (obj.type !== "tree") throw new VirtualOriginUnavailableError(path, `Expected tree at origin, got ${obj.type}`);
|
|
16
|
+
return obj;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* 读取 blob 原始内容;缺失时抛 VirtualOriginUnavailableError
|
|
20
|
+
*/
|
|
21
|
+
function readRepoBlobContent(source, hash, path) {
|
|
22
|
+
const obj = tryReadObject(source, hash);
|
|
23
|
+
if (obj === void 0) throw new VirtualOriginUnavailableError(path, `Origin blob object missing: ${hash}`);
|
|
24
|
+
if (obj.type !== "blob") throw new VirtualOriginUnavailableError(path, `Expected blob at origin, got ${obj.type}`);
|
|
25
|
+
return obj.content;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* 根据 tree 条目构造节点 origin
|
|
29
|
+
*/
|
|
30
|
+
function treeEntryToNodeOrigin(entry) {
|
|
31
|
+
if (entry.mode === "40000") return {
|
|
32
|
+
kind: "repo-tree",
|
|
33
|
+
hash: entry.hash
|
|
34
|
+
};
|
|
35
|
+
if (entry.mode === "100644" || entry.mode === "100755" || entry.mode === "120000") return {
|
|
36
|
+
kind: "repo-blob",
|
|
37
|
+
mode: entry.mode,
|
|
38
|
+
hash: entry.hash
|
|
39
|
+
};
|
|
40
|
+
return {
|
|
41
|
+
kind: "repo-blob",
|
|
42
|
+
mode: "100644",
|
|
43
|
+
hash: entry.hash
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Git mode 转为 VirtualEntryKind
|
|
48
|
+
*/
|
|
49
|
+
function modeToVirtualEntryKind(mode) {
|
|
50
|
+
if (mode === "40000") return "tree";
|
|
51
|
+
if (mode === "120000") return "symlink";
|
|
52
|
+
return "blob";
|
|
53
|
+
}
|
|
54
|
+
//#endregion
|
|
55
|
+
export { modeToVirtualEntryKind, readRepoBlobContent, readRepoTree, treeEntryToNodeOrigin };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { NodeId } from "./ids.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/workdir/overlay.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* 目录 overlay 状态(挂在目录 SessionNode 上)
|
|
6
|
+
*/
|
|
7
|
+
interface DirectoryOverlay {
|
|
8
|
+
/** session 新增或覆盖:条目名 -> 绑定的 nodeId */
|
|
9
|
+
readonly addedEntries: Map<string, NodeId>;
|
|
10
|
+
/** session 删除的 origin/先前条目名(tombstone) */
|
|
11
|
+
readonly deletedNames: Set<string>;
|
|
12
|
+
}
|
|
13
|
+
//#endregion
|
|
14
|
+
export { DirectoryOverlay };
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
//#region src/workdir/overlay.ts
|
|
2
|
+
/**
|
|
3
|
+
* 创建空目录 overlay
|
|
4
|
+
*/
|
|
5
|
+
function createEmptyDirectoryOverlay() {
|
|
6
|
+
return {
|
|
7
|
+
addedEntries: /* @__PURE__ */ new Map(),
|
|
8
|
+
deletedNames: /* @__PURE__ */ new Set()
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* 克隆目录 overlay(深拷贝 Map/Set)
|
|
13
|
+
*/
|
|
14
|
+
function cloneDirectoryOverlay(overlay) {
|
|
15
|
+
return {
|
|
16
|
+
addedEntries: new Map(overlay.addedEntries),
|
|
17
|
+
deletedNames: new Set(overlay.deletedNames)
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* 按 RFC 规则合成目录子项列表
|
|
22
|
+
*
|
|
23
|
+
* 1. 从 origin 去掉 deletedNames
|
|
24
|
+
* 2. addedEntries 覆盖同名 origin
|
|
25
|
+
* 3. 仅存在于 addedEntries 的条目追加
|
|
26
|
+
* 4. 按 Git tree 名称排序
|
|
27
|
+
*/
|
|
28
|
+
function mergeDirectoryChildren(originChildren, overlay, addedEntryModes) {
|
|
29
|
+
const byName = /* @__PURE__ */ new Map();
|
|
30
|
+
for (const child of originChildren) {
|
|
31
|
+
if (overlay.deletedNames.has(child.name)) continue;
|
|
32
|
+
const overrideId = overlay.addedEntries.get(child.name);
|
|
33
|
+
if (overrideId !== void 0) {
|
|
34
|
+
const mode = addedEntryModes.get(child.name) ?? child.mode;
|
|
35
|
+
byName.set(child.name, {
|
|
36
|
+
name: child.name,
|
|
37
|
+
mode,
|
|
38
|
+
nodeId: overrideId
|
|
39
|
+
});
|
|
40
|
+
} else byName.set(child.name, {
|
|
41
|
+
name: child.name,
|
|
42
|
+
mode: child.mode,
|
|
43
|
+
nodeId: child.nodeId
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
for (const [name, nodeId] of overlay.addedEntries) {
|
|
47
|
+
if (byName.has(name)) continue;
|
|
48
|
+
const mode = addedEntryModes.get(name);
|
|
49
|
+
if (mode === void 0) throw new Error(`Missing mode for added directory entry '${name}'`);
|
|
50
|
+
byName.set(name, {
|
|
51
|
+
name,
|
|
52
|
+
mode,
|
|
53
|
+
nodeId
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
const merged = Array.from(byName.values());
|
|
57
|
+
merged.sort((a, b) => a.name.localeCompare(b.name));
|
|
58
|
+
return merged;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* 在目录 overlay 中绑定或覆盖条目(create / modify / rename 目标 / copy 目标)
|
|
62
|
+
*/
|
|
63
|
+
function overlayBindEntry(overlay, name, nodeId) {
|
|
64
|
+
const addedEntries = new Map(overlay.addedEntries);
|
|
65
|
+
const deletedNames = new Set(overlay.deletedNames);
|
|
66
|
+
addedEntries.set(name, nodeId);
|
|
67
|
+
deletedNames.delete(name);
|
|
68
|
+
return {
|
|
69
|
+
addedEntries,
|
|
70
|
+
deletedNames
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* 在目录 overlay 中删除条目(tombstone;不递归清理子节点存储)
|
|
75
|
+
*/
|
|
76
|
+
function overlayTombstoneEntry(overlay, name) {
|
|
77
|
+
const addedEntries = new Map(overlay.addedEntries);
|
|
78
|
+
const deletedNames = new Set(overlay.deletedNames);
|
|
79
|
+
if (addedEntries.has(name)) addedEntries.delete(name);
|
|
80
|
+
else deletedNames.add(name);
|
|
81
|
+
return {
|
|
82
|
+
addedEntries,
|
|
83
|
+
deletedNames
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* 在同一目录内重命名:复用 nodeId,仅改绑定名
|
|
88
|
+
*/
|
|
89
|
+
function overlayRenameEntry(overlay, fromName, toName, nodeId) {
|
|
90
|
+
let next = overlayTombstoneEntry(overlay, fromName);
|
|
91
|
+
next = overlayBindEntry(next, toName, nodeId);
|
|
92
|
+
return next;
|
|
93
|
+
}
|
|
94
|
+
//#endregion
|
|
95
|
+
export { cloneDirectoryOverlay, createEmptyDirectoryOverlay, mergeDirectoryChildren, overlayBindEntry, overlayRenameEntry, overlayTombstoneEntry };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 规范化 readdir 等 API 的目录路径参数
|
|
3
|
+
*
|
|
4
|
+
* `undefined` 与 `""` 均视为根目录。
|
|
5
|
+
*/
|
|
6
|
+
function normalizeDirectoryPath(path) {
|
|
7
|
+
if (path === void 0 || path === "") return "";
|
|
8
|
+
assertValidVirtualPath(path);
|
|
9
|
+
return path;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* 校验非根虚拟路径格式(用于文件/子路径操作)
|
|
13
|
+
*
|
|
14
|
+
* @throws 路径为空或格式非法时抛出 Error
|
|
15
|
+
*/
|
|
16
|
+
function assertValidVirtualPath(path) {
|
|
17
|
+
if (path === "") throw new Error("Path must not be empty");
|
|
18
|
+
validateVirtualPathSegments(path);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* 校验路径段规则(根路径 `""` 由调用方单独处理)
|
|
22
|
+
*/
|
|
23
|
+
function validateVirtualPathSegments(path) {
|
|
24
|
+
if (path.startsWith("/")) throw new Error(`Path must not start with '/': ${path}`);
|
|
25
|
+
if (path.endsWith("/")) throw new Error(`Path must not end with '/': ${path}`);
|
|
26
|
+
if (path.includes("//")) throw new Error(`Path must not contain consecutive slashes: ${path}`);
|
|
27
|
+
for (const segment of path.split("/")) {
|
|
28
|
+
if (segment === "." || segment === "..") throw new Error(`Path must not contain '.' or '..': ${path}`);
|
|
29
|
+
if (segment === "") throw new Error(`Path must not contain empty segments: ${path}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 将路径拆分为段(不含空段)
|
|
34
|
+
*
|
|
35
|
+
* 调用前须保证路径已通过 `assertValidVirtualPath`。
|
|
36
|
+
*/
|
|
37
|
+
function splitPathSegments(path) {
|
|
38
|
+
return path.split("/");
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* 返回父路径;单段路径返回 `null`(表示无父目录,父为根)
|
|
42
|
+
*/
|
|
43
|
+
function parentPath(path) {
|
|
44
|
+
assertValidVirtualPath(path);
|
|
45
|
+
const lastSlash = path.lastIndexOf("/");
|
|
46
|
+
if (lastSlash === -1) return null;
|
|
47
|
+
return path.slice(0, lastSlash);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* 返回路径最后一段(条目名)
|
|
51
|
+
*/
|
|
52
|
+
function baseName(path) {
|
|
53
|
+
assertValidVirtualPath(path);
|
|
54
|
+
const lastSlash = path.lastIndexOf("/");
|
|
55
|
+
return lastSlash === -1 ? path : path.slice(lastSlash + 1);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* 组合父路径与条目名
|
|
59
|
+
*
|
|
60
|
+
* @param parent - `null` 表示根下直接子项
|
|
61
|
+
*/
|
|
62
|
+
function joinPath(parent, name) {
|
|
63
|
+
if (name === "" || name.includes("/")) throw new Error(`Invalid entry name: ${name}`);
|
|
64
|
+
if (parent === null) return name;
|
|
65
|
+
if (parent === "") return name;
|
|
66
|
+
assertValidVirtualPath(parent);
|
|
67
|
+
return `${parent}/${name}`;
|
|
68
|
+
}
|
|
69
|
+
//#endregion
|
|
70
|
+
export { assertValidVirtualPath, baseName, joinPath, normalizeDirectoryPath, parentPath, splitPathSegments };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
//#region src/workdir/session-id.ts
|
|
2
|
+
let nextSessionCounter = 1;
|
|
3
|
+
/**
|
|
4
|
+
* 分配新的 session ID
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* const sessionId = createVirtualWorkdirSessionId();
|
|
9
|
+
* expect(String(sessionId).startsWith("session:")).toBe(true);
|
|
10
|
+
* ```
|
|
11
|
+
*/
|
|
12
|
+
function createVirtualWorkdirSessionId() {
|
|
13
|
+
const id = `session:${nextSessionCounter}`;
|
|
14
|
+
nextSessionCounter += 1;
|
|
15
|
+
return id;
|
|
16
|
+
}
|
|
17
|
+
//#endregion
|
|
18
|
+
export { createVirtualWorkdirSessionId };
|