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
package/dist/workdir/core.d.mts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { SHA1 } from "../core/types.mjs";
|
|
2
|
-
import { ObjectDatabase } from "../core/types/odb.mjs";
|
|
3
2
|
import { VirtualNotDirectoryError, VirtualNotFileError, VirtualNotSymlinkError, VirtualOriginUnavailableError, VirtualPathAlreadyExistsError, VirtualPathNotFoundError, VirtualRevertNotSupportedError } from "../core/errors.mjs";
|
|
4
3
|
|
|
5
4
|
//#region src/workdir/core.d.ts
|
|
@@ -40,62 +39,89 @@ interface VirtualDirEntry {
|
|
|
40
39
|
readonly mode: string;
|
|
41
40
|
}
|
|
42
41
|
/**
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* 用于 `listChanges()` 返回的变更记录。
|
|
42
|
+
* diff 中的对象描述
|
|
46
43
|
*/
|
|
47
|
-
|
|
44
|
+
interface VirtualDiffObject {
|
|
45
|
+
/** 条目种类 */
|
|
46
|
+
readonly kind: "blob" | "symlink";
|
|
47
|
+
/** Git 文件模式 */
|
|
48
|
+
readonly mode: "100644" | "100755" | "120000";
|
|
49
|
+
/** 对象哈希 */
|
|
50
|
+
readonly hash: SHA1;
|
|
51
|
+
}
|
|
48
52
|
/**
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* 由 `listChanges()` 返回,描述 session 内的单次操作。
|
|
52
|
-
* 变更记录是会话内调试/测试辅助,不保证是最小 diff。
|
|
53
|
+
* rename/copy 来源描述
|
|
53
54
|
*/
|
|
54
|
-
interface
|
|
55
|
-
/**
|
|
55
|
+
interface VirtualDiffSource {
|
|
56
|
+
/** 来源类型 */
|
|
57
|
+
readonly kind: "rename" | "copy";
|
|
58
|
+
/** 来源路径 */
|
|
56
59
|
readonly path: string;
|
|
57
|
-
/** 变更操作类型 */
|
|
58
|
-
readonly type: VirtualChangeType;
|
|
59
|
-
/** rename/copy 操作的源路径(其他操作为 undefined) */
|
|
60
|
-
readonly oldPath?: string;
|
|
61
60
|
}
|
|
62
61
|
/**
|
|
63
|
-
*
|
|
62
|
+
* 同路径更新的变化维度
|
|
64
63
|
*/
|
|
65
|
-
interface
|
|
66
|
-
/**
|
|
67
|
-
readonly
|
|
64
|
+
interface VirtualDiffChanges {
|
|
65
|
+
/** 条目种类是否变化 */
|
|
66
|
+
readonly kindChanged: boolean;
|
|
67
|
+
/** mode 是否变化 */
|
|
68
|
+
readonly modeChanged: boolean;
|
|
69
|
+
/** 内容哈希是否变化 */
|
|
70
|
+
readonly contentChanged: boolean;
|
|
68
71
|
}
|
|
69
72
|
/**
|
|
70
|
-
*
|
|
73
|
+
* 单条 diff 条目
|
|
74
|
+
*
|
|
75
|
+
* 仅描述最终状态,不表达完整会话内操作历史。
|
|
71
76
|
*/
|
|
72
|
-
type
|
|
73
|
-
readonly
|
|
77
|
+
type VirtualDiffEntry = {
|
|
78
|
+
/** 新建路径 */readonly kind: "create"; /** 当前路径 */
|
|
79
|
+
readonly path: string; /** 当前对象 */
|
|
80
|
+
readonly current: VirtualDiffObject; /** rename/copy 的来源 */
|
|
81
|
+
readonly source?: VirtualDiffSource;
|
|
82
|
+
} | {
|
|
83
|
+
/** 删除路径 */readonly kind: "remove"; /** 当前路径 */
|
|
84
|
+
readonly path: string; /** 删除前对象 */
|
|
85
|
+
readonly previous: VirtualDiffObject;
|
|
86
|
+
} | {
|
|
87
|
+
/** 同路径更新 */readonly kind: "update"; /** 当前路径 */
|
|
88
|
+
readonly path: string; /** 更新前对象 */
|
|
89
|
+
readonly previous: VirtualDiffObject; /** 更新后对象 */
|
|
90
|
+
readonly current: VirtualDiffObject; /** 变化维度 */
|
|
91
|
+
readonly changes: VirtualDiffChanges; /** rename/copy 的来源 */
|
|
92
|
+
readonly source?: VirtualDiffSource;
|
|
74
93
|
};
|
|
75
94
|
/**
|
|
76
|
-
*
|
|
95
|
+
* 创建 VirtualWorkdir 的选项
|
|
96
|
+
*/
|
|
97
|
+
interface CreateVirtualWorkdirOptions {
|
|
98
|
+
/** 基线 tree 的 SHA-1 哈希 */
|
|
99
|
+
readonly baseTree: SHA1;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* VirtualWorkdir(虚拟工作目录实例)
|
|
77
103
|
*
|
|
78
104
|
* 提供独立生命周期的可变 tree 视图,基于 `baseTree + CoW overlay` 模型。
|
|
79
105
|
* 不绑定 commit,不涉及 Git index / 真实工作目录。
|
|
80
106
|
*
|
|
81
|
-
*
|
|
107
|
+
* 当前实例对 origin 仓库对象采用弱保证:
|
|
82
108
|
* 如果 base tree / origin blob 在后续被移除、损坏或不可读取,
|
|
83
109
|
* 相关读取、`revert()`、`writeTree()` 等操作会抛出 `VirtualOriginUnavailableError`。
|
|
84
110
|
*
|
|
85
111
|
* @example
|
|
86
112
|
* ```ts
|
|
87
113
|
* import { createMemoryRepository } from "nano-git/repository/memory";
|
|
88
|
-
* import {
|
|
114
|
+
* import { createVirtualWorkdir } from "nano-git/workdir/memory";
|
|
89
115
|
*
|
|
90
116
|
* const repo = createMemoryRepository();
|
|
91
117
|
* const tree = repo.writeTree(); // 初始空 tree
|
|
92
|
-
* const
|
|
118
|
+
* const workdir = createVirtualWorkdir(repo.objects, { baseTree: tree });
|
|
93
119
|
*
|
|
94
|
-
*
|
|
95
|
-
* const newTree =
|
|
120
|
+
* workdir.writeFile("hello.txt", Buffer.from("world"));
|
|
121
|
+
* const newTree = workdir.writeTree();
|
|
96
122
|
* ```
|
|
97
123
|
*/
|
|
98
|
-
interface
|
|
124
|
+
interface VirtualWorkdir {
|
|
99
125
|
/** 当前基线 tree 的 SHA-1 哈希 */
|
|
100
126
|
readonly baseTree: SHA1;
|
|
101
127
|
/** 路径是否存在 */
|
|
@@ -128,7 +154,7 @@ interface VirtualWorkdirSession {
|
|
|
128
154
|
/**
|
|
129
155
|
* 复制路径
|
|
130
156
|
*
|
|
131
|
-
* 新建
|
|
157
|
+
* 新建 workdir node,共享 origin,不共享 node 身份。
|
|
132
158
|
* 目录复制为浅复制,子项保持懒加载。
|
|
133
159
|
*/
|
|
134
160
|
copy(from: string, to: string): void;
|
|
@@ -139,6 +165,12 @@ interface VirtualWorkdirSession {
|
|
|
139
165
|
* 对纯新建节点抛出 VirtualRevertNotSupportedError。
|
|
140
166
|
*/
|
|
141
167
|
revert(path: string): void;
|
|
168
|
+
/**
|
|
169
|
+
* 读取最终 diff
|
|
170
|
+
*
|
|
171
|
+
* 输出按路径稳定排序,仅包含文件与符号链接条目。
|
|
172
|
+
*/
|
|
173
|
+
diff(): VirtualDiffEntry[];
|
|
142
174
|
/**
|
|
143
175
|
* 导出当前 overlay 为新 tree
|
|
144
176
|
*
|
|
@@ -147,42 +179,11 @@ interface VirtualWorkdirSession {
|
|
|
147
179
|
*/
|
|
148
180
|
writeTree(): SHA1;
|
|
149
181
|
/**
|
|
150
|
-
*
|
|
182
|
+
* 重置当前实例到指定基线 tree
|
|
151
183
|
*
|
|
152
184
|
* 丢弃全部 overlay 与变更历史。
|
|
153
185
|
*/
|
|
154
186
|
reset(baseTree: SHA1): void;
|
|
155
|
-
/**
|
|
156
|
-
* 列出会话内的变更记录
|
|
157
|
-
*
|
|
158
|
-
* 是会话内调试/测试辅助,不保证是最小 diff 引擎。
|
|
159
|
-
* 输出稳定、测试可断言即可。
|
|
160
|
-
*/
|
|
161
|
-
listChanges(): VirtualChange[];
|
|
162
|
-
}
|
|
163
|
-
/**
|
|
164
|
-
* VirtualWorkdirBackend
|
|
165
|
-
*
|
|
166
|
-
* session 内部状态存储的抽象接口。
|
|
167
|
-
* memory / file / sqlite 后端通过实现此接口来提供不同的持久化策略。
|
|
168
|
-
*
|
|
169
|
-
* `file` / `sqlite` 持久化 backend 当前都按单进程、单写者场景收口;
|
|
170
|
-
* 不承诺跨进程并发写安全,也不提供多写者协调协议。
|
|
171
|
-
*
|
|
172
|
-
* 本接口在后续 Phase 中会逐步补充完整方法签名。
|
|
173
|
-
* 当前仅为命名冻结与角色声明。
|
|
174
|
-
*/
|
|
175
|
-
interface VirtualWorkdirBackend {
|
|
176
|
-
/** 后端类型标识 */
|
|
177
|
-
readonly kind: "memory" | "file" | "sqlite";
|
|
178
|
-
/** 创建新 session 并返回其标识 */
|
|
179
|
-
createSession(options: CreateVirtualWorkdirSessionOptions): VirtualWorkdirSessionId;
|
|
180
|
-
/** 打开已存在的 session */
|
|
181
|
-
openSession(source: ObjectDatabase, sessionId: VirtualWorkdirSessionId): VirtualWorkdirSession;
|
|
182
|
-
/** 删除 session */
|
|
183
|
-
deleteSession(sessionId: VirtualWorkdirSessionId): void;
|
|
184
|
-
/** 列出当前后端中可完整恢复的 session */
|
|
185
|
-
listSessions(): VirtualWorkdirSessionId[];
|
|
186
187
|
}
|
|
187
188
|
//#endregion
|
|
188
|
-
export {
|
|
189
|
+
export { CreateVirtualWorkdirOptions, VirtualDiffChanges, VirtualDiffEntry, VirtualDiffObject, VirtualDiffSource, VirtualDirEntry, VirtualEntryKind, VirtualEntryStat, VirtualNotDirectoryError, VirtualNotFileError, VirtualNotSymlinkError, VirtualOriginUnavailableError, VirtualPathAlreadyExistsError, VirtualPathNotFoundError, VirtualRevertNotSupportedError, VirtualWorkdir };
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { VirtualNotDirectoryError } from "../core/errors.mjs";
|
|
2
|
+
import { ensureNodeFromTreeEntry, joinChildPath, listDirectoryChildren } from "./workdir-path.mjs";
|
|
3
|
+
//#region src/workdir/directory-view.ts
|
|
4
|
+
/**
|
|
5
|
+
* Virtual Workdir 目录展开、观察与编译计划
|
|
6
|
+
*
|
|
7
|
+
* 从 workdir-path.ts 拆分,聚焦以下职责:
|
|
8
|
+
* - 目录子项观察(observeDirectoryChildren / observeListedDirectoryChild / observeNamedDirectoryChild)
|
|
9
|
+
* - origin 按名查询视图(createNamedOriginChildLookup)
|
|
10
|
+
* - 受影响子项编译计划(planAffectedDirectoryChildren)
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* 在不展开整个目录列表的前提下,按名称定向解析单个子节点。
|
|
14
|
+
*
|
|
15
|
+
* 优先读取 overlay 绑定;若 overlay 未覆盖,再按需从 origin 条目懒注册。
|
|
16
|
+
*/
|
|
17
|
+
function resolveNamedChild(state, dirNode, originLookup, name) {
|
|
18
|
+
if (dirNode.state.kind !== "directory") return {
|
|
19
|
+
found: false,
|
|
20
|
+
node: null
|
|
21
|
+
};
|
|
22
|
+
const overlayNodeId = dirNode.state.overlay.addedEntries.get(name);
|
|
23
|
+
if (overlayNodeId !== void 0) {
|
|
24
|
+
const overlayNode = state.getNode(overlayNodeId);
|
|
25
|
+
return overlayNode === null ? {
|
|
26
|
+
found: false,
|
|
27
|
+
node: null
|
|
28
|
+
} : {
|
|
29
|
+
found: true,
|
|
30
|
+
node: overlayNode
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
if (dirNode.state.overlay.deletedNames.has(name)) return {
|
|
34
|
+
found: false,
|
|
35
|
+
node: null
|
|
36
|
+
};
|
|
37
|
+
const originEntry = originLookup.get(name);
|
|
38
|
+
if (originEntry === void 0) return {
|
|
39
|
+
found: false,
|
|
40
|
+
node: null
|
|
41
|
+
};
|
|
42
|
+
const originNodeId = ensureNodeFromTreeEntry(state, originEntry);
|
|
43
|
+
const originNode = state.getNode(originNodeId);
|
|
44
|
+
return originNode === null ? {
|
|
45
|
+
found: false,
|
|
46
|
+
node: null
|
|
47
|
+
} : {
|
|
48
|
+
found: true,
|
|
49
|
+
node: originNode
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* 基于已展开的目录子项读取当前节点与完整路径。
|
|
54
|
+
*
|
|
55
|
+
* 节点不存在时返回 `null`。
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* ```ts
|
|
59
|
+
* const observed = observeListedDirectoryChild(state, "", child);
|
|
60
|
+
* expect(observed?.path).toBe("a.txt");
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
function observeListedDirectoryChild(state, dirPath, child) {
|
|
64
|
+
const node = state.getNode(child.nodeId);
|
|
65
|
+
if (node === null) return null;
|
|
66
|
+
return {
|
|
67
|
+
name: child.name,
|
|
68
|
+
path: joinChildPath(dirPath, child.name),
|
|
69
|
+
child,
|
|
70
|
+
node
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* 基于目录节点与子项名称定向读取当前节点与完整路径。
|
|
75
|
+
*
|
|
76
|
+
* 适用于已持有 origin lookup、但不想手工重复拼装 `{ name, path, node }`
|
|
77
|
+
* 局部协议的场景。
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* ```ts
|
|
81
|
+
* const observed = observeNamedDirectoryChild(state, dirNode, "", lookup, "a.txt");
|
|
82
|
+
* expect(observed?.path).toBe("a.txt");
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
function observeNamedDirectoryChild(state, dirNode, dirPath, originLookup, name) {
|
|
86
|
+
if (dirNode.state.kind !== "directory") return null;
|
|
87
|
+
const resolved = resolveNamedChild(state, dirNode, originLookup, name);
|
|
88
|
+
if (!resolved.found) return null;
|
|
89
|
+
return {
|
|
90
|
+
name,
|
|
91
|
+
path: joinChildPath(dirPath, name),
|
|
92
|
+
node: resolved.node
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* 观察目录当前子项,归纳直接受影响名字与更深层脏项数。
|
|
97
|
+
*
|
|
98
|
+
* 目录自身 overlay 的 add/delete 会直接计入 `affectedNames`;
|
|
99
|
+
* 子目录是否脏、叶子节点是否脏由调用方回调决定。
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* ```ts
|
|
103
|
+
* const observed = observeDirectoryChildren(source, state, node, "", {
|
|
104
|
+
* onDirectoryChild() {
|
|
105
|
+
* return 0;
|
|
106
|
+
* },
|
|
107
|
+
* isLeafChildDirty() {
|
|
108
|
+
* return false;
|
|
109
|
+
* },
|
|
110
|
+
* });
|
|
111
|
+
* expect(observed.affectedNames.size).toBeGreaterThanOrEqual(0);
|
|
112
|
+
* ```
|
|
113
|
+
*/
|
|
114
|
+
function observeDirectoryChildren(source, state, dirNode, dirPath, options) {
|
|
115
|
+
if (dirNode.state.kind !== "directory") throw new VirtualNotDirectoryError(dirPath);
|
|
116
|
+
const affectedNames = /* @__PURE__ */ new Set([...dirNode.state.overlay.addedEntries.keys(), ...dirNode.state.overlay.deletedNames.values()]);
|
|
117
|
+
let dirtyDescendantCount = 0;
|
|
118
|
+
const children = listDirectoryChildren(source, state, dirNode, dirPath);
|
|
119
|
+
for (const child of children) {
|
|
120
|
+
const observedChild = observeListedDirectoryChild(state, dirPath, child);
|
|
121
|
+
if (observedChild === null) continue;
|
|
122
|
+
if (observedChild.node.state.kind === "directory") {
|
|
123
|
+
const childDirtyCount = options.onDirectoryChild({
|
|
124
|
+
name: observedChild.name,
|
|
125
|
+
path: observedChild.path,
|
|
126
|
+
node: observedChild.node
|
|
127
|
+
});
|
|
128
|
+
if (childDirtyCount > 0) {
|
|
129
|
+
affectedNames.add(observedChild.name);
|
|
130
|
+
dirtyDescendantCount += childDirtyCount;
|
|
131
|
+
}
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (options.isLeafChildDirty({
|
|
135
|
+
name: observedChild.name,
|
|
136
|
+
path: observedChild.path,
|
|
137
|
+
node: observedChild.node
|
|
138
|
+
})) affectedNames.add(observedChild.name);
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
affectedNames,
|
|
142
|
+
dirtyDescendantCount
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* 为目录 origin 条目创建按名查询视图。
|
|
147
|
+
*
|
|
148
|
+
* @example
|
|
149
|
+
* ```ts
|
|
150
|
+
* const lookup = createNamedOriginChildLookup(tree.entries);
|
|
151
|
+
* expect(lookup.has("a.txt")).toBe(true);
|
|
152
|
+
* ```
|
|
153
|
+
*/
|
|
154
|
+
function createNamedOriginChildLookup(entries) {
|
|
155
|
+
const entriesByName = new Map(entries.map((entry) => [entry.name, entry]));
|
|
156
|
+
return {
|
|
157
|
+
entries,
|
|
158
|
+
has(name) {
|
|
159
|
+
return entriesByName.has(name);
|
|
160
|
+
},
|
|
161
|
+
get(name) {
|
|
162
|
+
return entriesByName.get(name);
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* 基于 origin 顺序和受影响名字生成目录子项编译计划。
|
|
168
|
+
*
|
|
169
|
+
* 1. origin 中未受影响的条目保持原顺序并标记为直接复用
|
|
170
|
+
* 2. origin 中受影响的条目保持原顺序并标记为需要编译
|
|
171
|
+
* 3. 不在 origin 中的受影响名字补在末尾
|
|
172
|
+
*
|
|
173
|
+
* @example
|
|
174
|
+
* ```ts
|
|
175
|
+
* const plan = planAffectedDirectoryChildren(lookup, new Set(["b.txt", "c.txt"]));
|
|
176
|
+
* expect(plan.map((entry) => entry.name)).toEqual(["a.txt", "b.txt", "c.txt"]);
|
|
177
|
+
* ```
|
|
178
|
+
*/
|
|
179
|
+
function planAffectedDirectoryChildren(originLookup, affectedNames) {
|
|
180
|
+
const out = [];
|
|
181
|
+
for (const entry of originLookup.entries) out.push({
|
|
182
|
+
name: entry.name,
|
|
183
|
+
originEntry: entry,
|
|
184
|
+
shouldCompile: affectedNames.has(entry.name)
|
|
185
|
+
});
|
|
186
|
+
for (const name of affectedNames) {
|
|
187
|
+
if (originLookup.has(name)) continue;
|
|
188
|
+
out.push({
|
|
189
|
+
name,
|
|
190
|
+
originEntry: null,
|
|
191
|
+
shouldCompile: true
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
return out;
|
|
195
|
+
}
|
|
196
|
+
//#endregion
|
|
197
|
+
export { createNamedOriginChildLookup, observeDirectoryChildren, observeListedDirectoryChild, observeNamedDirectoryChild, planAffectedDirectoryChildren };
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { getRootNode } from "./workdir-path.mjs";
|
|
2
|
+
import { observeDirectoryChildren } from "./directory-view.mjs";
|
|
3
|
+
import { createDirtyDirSummary } from "./dirty-dir.mjs";
|
|
4
|
+
//#region src/workdir/dirty-dir-plan.ts
|
|
5
|
+
/**
|
|
6
|
+
* Virtual Workdir dirty-dir summary 重建策略
|
|
7
|
+
*
|
|
8
|
+
* 把 workdir 写路径里的脏目录摘要清理与重建逻辑集中到单独模块,
|
|
9
|
+
* 让 workdir.ts 更接近纯编排层。
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* 创建 dirty-dir summary 策略器。
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* const planner = createDirtyDirPlanner(source, state);
|
|
17
|
+
* planner.rebuild(["src/index.ts"]);
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
function createDirtyDirPlanner(source, state) {
|
|
21
|
+
return {
|
|
22
|
+
clear() {
|
|
23
|
+
for (const summary of state.listDirtyDirSummaries()) state.deleteDirtyDirSummary(summary.path);
|
|
24
|
+
},
|
|
25
|
+
rebuild(touchedPaths) {
|
|
26
|
+
const nextSummaries = /* @__PURE__ */ new Map();
|
|
27
|
+
const invalidatedDirPaths = collectInvalidatedSummaryPaths(touchedPaths);
|
|
28
|
+
const visitDirectory = (node, dirPath) => {
|
|
29
|
+
if (node.state.kind !== "directory") throw new Error(`rebuildDirtyDirectorySummaries: '${dirPath}' is not a directory`);
|
|
30
|
+
const { affectedNames, dirtyDescendantCount } = observeDirectoryChildren(source, state, node, dirPath, {
|
|
31
|
+
onDirectoryChild(child) {
|
|
32
|
+
return visitDirectory(child.node, child.path);
|
|
33
|
+
},
|
|
34
|
+
isLeafChildDirty(child) {
|
|
35
|
+
return state.getChangeRecord(child.path) !== null;
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
if (affectedNames.size === 0 && dirtyDescendantCount === 0) return 0;
|
|
39
|
+
const existing = state.getDirtyDirSummary(dirPath);
|
|
40
|
+
const preserveHash = existing !== null && !invalidatedDirPaths.has(dirPath);
|
|
41
|
+
nextSummaries.set(dirPath, {
|
|
42
|
+
...createDirtyDirSummary(dirPath, Array.from(affectedNames)),
|
|
43
|
+
dirtyDescendantCount,
|
|
44
|
+
currentTreeHash: preserveHash ? existing.currentTreeHash : null,
|
|
45
|
+
hashState: preserveHash ? existing.hashState : "stale"
|
|
46
|
+
});
|
|
47
|
+
return affectedNames.size + dirtyDescendantCount;
|
|
48
|
+
};
|
|
49
|
+
visitDirectory(getRootNode(state), "");
|
|
50
|
+
for (const summary of state.listDirtyDirSummaries()) if (!nextSummaries.has(summary.path)) state.deleteDirtyDirSummary(summary.path);
|
|
51
|
+
for (const summary of nextSummaries.values()) state.setDirtyDirSummary(summary);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function collectInvalidatedSummaryPaths(paths) {
|
|
56
|
+
const out = /* @__PURE__ */ new Set();
|
|
57
|
+
for (const path of paths) {
|
|
58
|
+
out.add(path);
|
|
59
|
+
let cursor = path;
|
|
60
|
+
while (true) {
|
|
61
|
+
const slashIndex = cursor.lastIndexOf("/");
|
|
62
|
+
if (slashIndex < 0) {
|
|
63
|
+
out.add("");
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
cursor = cursor.slice(0, slashIndex);
|
|
67
|
+
out.add(cursor);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
//#endregion
|
|
73
|
+
export { createDirtyDirPlanner };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { SHA1 } from "../core/types.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/workdir/dirty-dir.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* 目录摘要中的 hash 状态
|
|
6
|
+
*/
|
|
7
|
+
type DirtyDirHashState = "stale" | "materialized";
|
|
8
|
+
/**
|
|
9
|
+
* 脏目录摘要
|
|
10
|
+
*/
|
|
11
|
+
interface DirtyDirSummary {
|
|
12
|
+
/** 目录路径,根目录为 "" */
|
|
13
|
+
readonly path: string;
|
|
14
|
+
/** 当前目录是否脏 */
|
|
15
|
+
readonly isDirty: boolean;
|
|
16
|
+
/** 当前目录下受影响的直接子项数量 */
|
|
17
|
+
readonly dirtyEntryCount: number;
|
|
18
|
+
/** 当前目录更深层受影响子项数量 */
|
|
19
|
+
readonly dirtyDescendantCount: number;
|
|
20
|
+
/** 当前目录下受影响的直接子项名称 */
|
|
21
|
+
readonly affectedNames: readonly string[];
|
|
22
|
+
/** 当前物化 tree hash;未计算时为 null */
|
|
23
|
+
readonly currentTreeHash: SHA1 | null;
|
|
24
|
+
/** 当前 tree hash 的可信状态 */
|
|
25
|
+
readonly hashState: DirtyDirHashState;
|
|
26
|
+
}
|
|
27
|
+
//#endregion
|
|
28
|
+
export { DirtyDirSummary };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
//#region src/workdir/dirty-dir.ts
|
|
2
|
+
/**
|
|
3
|
+
* 创建脏目录摘要记录
|
|
4
|
+
*/
|
|
5
|
+
function createDirtyDirSummary(path, affectedNames = []) {
|
|
6
|
+
const names = [...new Set(affectedNames)].sort((left, right) => left.localeCompare(right));
|
|
7
|
+
return {
|
|
8
|
+
path,
|
|
9
|
+
isDirty: true,
|
|
10
|
+
dirtyEntryCount: names.length,
|
|
11
|
+
dirtyDescendantCount: 0,
|
|
12
|
+
affectedNames: names,
|
|
13
|
+
currentTreeHash: null,
|
|
14
|
+
hashState: "stale"
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* 将目录摘要标记为已物化 tree hash。
|
|
19
|
+
*/
|
|
20
|
+
function materializeDirtyDirSummary(current, path, treeHash) {
|
|
21
|
+
return {
|
|
22
|
+
path,
|
|
23
|
+
isDirty: true,
|
|
24
|
+
dirtyEntryCount: current?.dirtyEntryCount ?? 0,
|
|
25
|
+
dirtyDescendantCount: current?.dirtyDescendantCount ?? 0,
|
|
26
|
+
affectedNames: [...current?.affectedNames ?? []].sort((left, right) => left.localeCompare(right)),
|
|
27
|
+
currentTreeHash: treeHash,
|
|
28
|
+
hashState: "materialized"
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
//#endregion
|
|
32
|
+
export { createDirtyDirSummary, materializeDirtyDirSummary };
|
|
@@ -1,22 +1,44 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ObjectDatabase } from "../core/types/odb.mjs";
|
|
2
|
+
import { CreateVirtualWorkdirOptions, VirtualWorkdir } from "./core.mjs";
|
|
3
|
+
import { VirtualWorkdirStateStore } from "./state-store.mjs";
|
|
2
4
|
|
|
3
5
|
//#region src/workdir/file-backend.d.ts
|
|
4
|
-
/**
|
|
5
|
-
interface
|
|
6
|
-
/**
|
|
7
|
-
readonly
|
|
6
|
+
/** 打开文件系统 VirtualWorkdir 的可选参数 */
|
|
7
|
+
interface OpenFileVirtualWorkdirOptions extends CreateVirtualWorkdirOptions {
|
|
8
|
+
/** 不存在时按 baseTree 初始化 */
|
|
9
|
+
readonly create?: boolean;
|
|
8
10
|
}
|
|
9
11
|
/**
|
|
10
|
-
*
|
|
12
|
+
* 打开基于目录持久化的 VirtualWorkdir
|
|
11
13
|
*
|
|
12
14
|
* @example
|
|
13
15
|
* ```ts
|
|
14
|
-
* const
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
16
|
+
* const workdir = openFileVirtualWorkdir(repo.objects, "/tmp/workdir", {
|
|
17
|
+
* baseTree: tree,
|
|
18
|
+
* create: true,
|
|
19
|
+
* });
|
|
20
|
+
* expect(workdir.baseTree).toBe(tree);
|
|
18
21
|
* ```
|
|
19
22
|
*/
|
|
20
|
-
declare function
|
|
23
|
+
declare function openFileVirtualWorkdir(source: ObjectDatabase, workdirDir: string, options: OpenFileVirtualWorkdirOptions): VirtualWorkdir;
|
|
24
|
+
/**
|
|
25
|
+
* 删除指定目录上的持久化 VirtualWorkdir
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```ts
|
|
29
|
+
* deleteFileVirtualWorkdir("/tmp/workdir");
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
declare function deleteFileVirtualWorkdir(workdirDir: string): void;
|
|
33
|
+
/**
|
|
34
|
+
* 创建单个文件系统 VirtualWorkdir 的状态存储
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```ts
|
|
38
|
+
* const store = createFileVirtualWorkdirStateStore("/tmp/workdir");
|
|
39
|
+
* expect(store.kind).toBe("file");
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
declare function createFileVirtualWorkdirStateStore(workdirDir: string): VirtualWorkdirStateStore;
|
|
21
43
|
//#endregion
|
|
22
|
-
export {
|
|
44
|
+
export { OpenFileVirtualWorkdirOptions, createFileVirtualWorkdirStateStore, deleteFileVirtualWorkdir, openFileVirtualWorkdir };
|