nano-git 0.2.0 → 0.2.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.
- 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.mjs +63 -0
- package/dist/workdir/core.d.mts +166 -0
- package/dist/workdir/core.mjs +2 -0
- package/dist/workdir/ids.mjs +20 -0
- package/dist/workdir/memory-backend.mjs +21 -0
- package/dist/workdir/memory.d.mts +2 -0
- package/dist/workdir/memory.mjs +2 -0
- package/dist/workdir/nodes.mjs +112 -0
- package/dist/workdir/origin.mjs +55 -0
- package/dist/workdir/overlay.mjs +95 -0
- package/dist/workdir/path.mjs +70 -0
- package/dist/workdir/session-internal.mjs +141 -0
- package/dist/workdir/session.d.mts +18 -0
- package/dist/workdir/session.mjs +380 -0
- package/dist/workdir/write-tree.mjs +97 -0
- package/package.json +9 -3
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { SHA1 } from "../core/types.mjs";
|
|
2
|
+
import { VirtualNotDirectoryError, VirtualNotFileError, VirtualNotSymlinkError, VirtualOriginUnavailableError, VirtualPathAlreadyExistsError, VirtualPathNotFoundError, VirtualRevertNotSupportedError } from "../core/errors.mjs";
|
|
3
|
+
|
|
4
|
+
//#region src/workdir/core.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* 虚拟工作目录条目种类
|
|
7
|
+
*
|
|
8
|
+
* - `"blob"`: 普通文件或可执行文件(mode 100644 / 100755)
|
|
9
|
+
* - `"tree"`: 目录(mode 40000)
|
|
10
|
+
* - `"symlink"`: 符号链接(mode 120000)
|
|
11
|
+
*/
|
|
12
|
+
type VirtualEntryKind = "blob" | "tree" | "symlink";
|
|
13
|
+
/**
|
|
14
|
+
* 虚拟路径状态信息
|
|
15
|
+
*
|
|
16
|
+
* 由 `stat()` 返回,描述路径对应的节点属性。
|
|
17
|
+
*/
|
|
18
|
+
interface VirtualEntryStat {
|
|
19
|
+
/** 条目种类 */
|
|
20
|
+
readonly kind: VirtualEntryKind;
|
|
21
|
+
/** Git 文件模式(如 "100644"、"100755"、"40000"、"120000") */
|
|
22
|
+
readonly mode: string;
|
|
23
|
+
/** 文件大小(对 blob 和 symlink 有效;目录返回 0) */
|
|
24
|
+
readonly size: number;
|
|
25
|
+
/** 内容哈希(对 repo-backed 节点返回 origin 哈希;CoW 节点可能为 null) */
|
|
26
|
+
readonly hash: SHA1 | null;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* 目录条目
|
|
30
|
+
*
|
|
31
|
+
* 由 `readdir()` 返回,描述目录下的子条目。
|
|
32
|
+
*/
|
|
33
|
+
interface VirtualDirEntry {
|
|
34
|
+
/** 条目名称(不含路径前缀) */
|
|
35
|
+
readonly name: string;
|
|
36
|
+
/** 条目种类 */
|
|
37
|
+
readonly kind: VirtualEntryKind;
|
|
38
|
+
/** Git 文件模式 */
|
|
39
|
+
readonly mode: string;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* 变更操作类型
|
|
43
|
+
*
|
|
44
|
+
* 用于 `listChanges()` 返回的变更记录。
|
|
45
|
+
*/
|
|
46
|
+
type VirtualChangeType = "add" | "modify" | "delete" | "rename" | "copy";
|
|
47
|
+
/**
|
|
48
|
+
* 单条变更记录
|
|
49
|
+
*
|
|
50
|
+
* 由 `listChanges()` 返回,描述 session 内的单次操作。
|
|
51
|
+
* 变更记录是会话内调试/测试辅助,不保证是最小 diff。
|
|
52
|
+
*/
|
|
53
|
+
interface VirtualChange {
|
|
54
|
+
/** 操作路径 */
|
|
55
|
+
readonly path: string;
|
|
56
|
+
/** 变更操作类型 */
|
|
57
|
+
readonly type: VirtualChangeType;
|
|
58
|
+
/** rename/copy 操作的源路径(其他操作为 undefined) */
|
|
59
|
+
readonly oldPath?: string;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* 创建 VirtualWorkdirSession 的选项
|
|
63
|
+
*/
|
|
64
|
+
interface CreateVirtualWorkdirSessionOptions {
|
|
65
|
+
/** 基线 tree 的 SHA-1 哈希 */
|
|
66
|
+
readonly baseTree: SHA1;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* VirtualWorkdirSession(虚拟工作目录会话)
|
|
70
|
+
*
|
|
71
|
+
* 提供独立生命周期的可变 tree 视图,基于 `baseTree + CoW overlay` 模型。
|
|
72
|
+
* 不绑定 commit,不涉及 Git index / 真实工作目录。
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```ts
|
|
76
|
+
* import { createMemoryRepository } from "nano-git/repository/memory";
|
|
77
|
+
* import { createVirtualWorkdirSession } from "nano-git/workdir/memory";
|
|
78
|
+
*
|
|
79
|
+
* const repo = createMemoryRepository();
|
|
80
|
+
* const tree = repo.writeTree(); // 初始空 tree
|
|
81
|
+
* const session = createVirtualWorkdirSession(repo, { baseTree: tree });
|
|
82
|
+
*
|
|
83
|
+
* session.writeFile("hello.txt", Buffer.from("world"));
|
|
84
|
+
* const newTree = session.writeTree();
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
interface VirtualWorkdirSession {
|
|
88
|
+
/** 当前基线 tree 的 SHA-1 哈希 */
|
|
89
|
+
readonly baseTree: SHA1;
|
|
90
|
+
/** 路径是否存在 */
|
|
91
|
+
exists(path: string): boolean;
|
|
92
|
+
/** 获取路径状态信息,不存在时返回 null */
|
|
93
|
+
stat(path: string): VirtualEntryStat | null;
|
|
94
|
+
/** 读取目录内容,根目录为 "" */
|
|
95
|
+
readdir(path?: string): VirtualDirEntry[];
|
|
96
|
+
/** 读取文件内容 */
|
|
97
|
+
readFile(path: string): Buffer;
|
|
98
|
+
/** 读取符号链接目标 */
|
|
99
|
+
readLink(path: string): string;
|
|
100
|
+
/** 写入文件(新建或覆盖) */
|
|
101
|
+
writeFile(path: string, content: Buffer, options?: {
|
|
102
|
+
readonly mode?: "100644" | "100755";
|
|
103
|
+
}): void;
|
|
104
|
+
/** 写入符号链接(新建或覆盖) */
|
|
105
|
+
writeLink(path: string, target: string): void;
|
|
106
|
+
/** 创建目录(含必要父目录) */
|
|
107
|
+
mkdir(path: string): void;
|
|
108
|
+
/** 删除路径(文件、目录或符号链接) */
|
|
109
|
+
delete(path: string): void;
|
|
110
|
+
/**
|
|
111
|
+
* 重命名路径
|
|
112
|
+
*
|
|
113
|
+
* 只做路径重绑定,不退化为 delete + write。
|
|
114
|
+
* 目录重命名后,子项保持懒加载。
|
|
115
|
+
*/
|
|
116
|
+
rename(from: string, to: string): void;
|
|
117
|
+
/**
|
|
118
|
+
* 复制路径
|
|
119
|
+
*
|
|
120
|
+
* 新建 session node,共享 origin,不共享 node 身份。
|
|
121
|
+
* 目录复制为浅复制,子项保持懒加载。
|
|
122
|
+
*/
|
|
123
|
+
copy(from: string, to: string): void;
|
|
124
|
+
/**
|
|
125
|
+
* 恢复路径到其 origin
|
|
126
|
+
*
|
|
127
|
+
* 仅对当前 CoW 节点恢复其 origin(repo-backed 版本)。
|
|
128
|
+
* 对纯新建节点抛出 VirtualRevertNotSupportedError。
|
|
129
|
+
*/
|
|
130
|
+
revert(path: string): void;
|
|
131
|
+
/**
|
|
132
|
+
* 导出当前 overlay 为新 tree
|
|
133
|
+
*
|
|
134
|
+
* 只重新合成受影响目录,复用未修改节点的哈希。
|
|
135
|
+
* 不自动推进 baseTree。
|
|
136
|
+
*/
|
|
137
|
+
writeTree(): SHA1;
|
|
138
|
+
/**
|
|
139
|
+
* 重置 session 到指定基线 tree
|
|
140
|
+
*
|
|
141
|
+
* 丢弃全部 overlay 与变更历史。
|
|
142
|
+
*/
|
|
143
|
+
reset(baseTree: SHA1): void;
|
|
144
|
+
/**
|
|
145
|
+
* 列出会话内的变更记录
|
|
146
|
+
*
|
|
147
|
+
* 是会话内调试/测试辅助,不保证是最小 diff 引擎。
|
|
148
|
+
* 输出稳定、测试可断言即可。
|
|
149
|
+
*/
|
|
150
|
+
listChanges(): VirtualChange[];
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* VirtualWorkdirBackend
|
|
154
|
+
*
|
|
155
|
+
* session 内部状态存储的抽象接口。
|
|
156
|
+
* memory / file / sqlite 后端通过实现此接口来提供不同的持久化策略。
|
|
157
|
+
*
|
|
158
|
+
* 本接口在后续 Phase 中会逐步补充完整方法签名。
|
|
159
|
+
* 当前仅为命名冻结与角色声明。
|
|
160
|
+
*/
|
|
161
|
+
interface VirtualWorkdirBackend {
|
|
162
|
+
/** 后端类型标识 */
|
|
163
|
+
readonly kind: "memory" | "file" | "sqlite";
|
|
164
|
+
}
|
|
165
|
+
//#endregion
|
|
166
|
+
export { CreateVirtualWorkdirSessionOptions, VirtualChange, VirtualChangeType, VirtualDirEntry, VirtualEntryKind, VirtualEntryStat, VirtualNotDirectoryError, VirtualNotFileError, VirtualNotSymlinkError, VirtualOriginUnavailableError, VirtualPathAlreadyExistsError, VirtualPathNotFoundError, VirtualRevertNotSupportedError, VirtualWorkdirBackend, VirtualWorkdirSession };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { VirtualNotDirectoryError, VirtualNotFileError, VirtualNotSymlinkError, VirtualOriginUnavailableError, VirtualPathAlreadyExistsError, VirtualPathNotFoundError, VirtualRevertNotSupportedError } from "../core/errors.mjs";
|
|
2
|
+
export { VirtualNotDirectoryError, VirtualNotFileError, VirtualNotSymlinkError, VirtualOriginUnavailableError, VirtualPathAlreadyExistsError, VirtualPathNotFoundError, VirtualRevertNotSupportedError };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
//#region src/workdir/ids.ts
|
|
2
|
+
/** 根目录节点的固定 ID(每个 session 一致) */
|
|
3
|
+
const VIRTUAL_ROOT_NODE_ID = "root";
|
|
4
|
+
let nextNodeCounter = 1;
|
|
5
|
+
/**
|
|
6
|
+
* 分配新的 session 节点 ID
|
|
7
|
+
*/
|
|
8
|
+
function createNodeId() {
|
|
9
|
+
const id = `node:${nextNodeCounter}`;
|
|
10
|
+
nextNodeCounter += 1;
|
|
11
|
+
return id;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* 为仓库对象哈希派生稳定的 session 节点 ID(懒加载、未 copy 前复用)
|
|
15
|
+
*/
|
|
16
|
+
function originBackedNodeId(hash) {
|
|
17
|
+
return `origin:${hash}`;
|
|
18
|
+
}
|
|
19
|
+
//#endregion
|
|
20
|
+
export { VIRTUAL_ROOT_NODE_ID, createNodeId, originBackedNodeId };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { VIRTUAL_ROOT_NODE_ID } from "./ids.mjs";
|
|
2
|
+
import { createVirtualChangeLog } from "./change-log.mjs";
|
|
3
|
+
import { createRootDirectoryNode } from "./nodes.mjs";
|
|
4
|
+
//#region src/workdir/memory-backend.ts
|
|
5
|
+
/**
|
|
6
|
+
* Virtual Workdir 会话内存状态(与 ODB 后端无关的纯状态容器)
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* 创建初始 session 状态(仅根目录节点,绑定 baseTree origin)
|
|
10
|
+
*/
|
|
11
|
+
function createVirtualWorkdirMemoryState(baseTree) {
|
|
12
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
13
|
+
nodes.set(VIRTUAL_ROOT_NODE_ID, createRootDirectoryNode(baseTree));
|
|
14
|
+
return {
|
|
15
|
+
baseTree,
|
|
16
|
+
nodes,
|
|
17
|
+
changeLog: createVirtualChangeLog()
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
//#endregion
|
|
21
|
+
export { createVirtualWorkdirMemoryState };
|
|
@@ -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,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,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.nodes.get(VIRTUAL_ROOT_NODE_ID);
|
|
17
|
+
if (root === void 0) 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.nodes.get(child.nodeId);
|
|
40
|
+
if (childNode === void 0) 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.nodes.get(child.nodeId);
|
|
65
|
+
if (childNode === void 0) 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.nodes.has(id)) {
|
|
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.nodes.set(id, {
|
|
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.nodes.get(nodeId);
|
|
131
|
+
if (node !== void 0) 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 };
|