nano-git 0.3.4 → 0.3.6
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/backend/sqlite-pool.d.mts +20 -0
- package/dist/backend/sqlite-pool.mjs +95 -0
- package/dist/backend/sqlite.d.mts +2 -1
- package/dist/backend/sqlite.mjs +16 -13
- package/dist/odb/sqlite.d.mts +6 -6
- package/dist/odb/sqlite.mjs +11 -11
- package/dist/refs/shallow/sqlite.d.mts +6 -6
- package/dist/refs/shallow/sqlite.mjs +12 -12
- package/dist/refs/sqlite.d.mts +6 -6
- package/dist/refs/sqlite.mjs +12 -11
- package/dist/repository/ops/object-operations.mjs +16 -0
- package/dist/workdir/change-index.d.mts +1 -1
- package/dist/workdir/change-index.mjs +8 -8
- package/dist/workdir/core.d.mts +10 -8
- package/dist/workdir/directory-view.mjs +2 -113
- package/dist/workdir/file-backend.mjs +12 -1
- package/dist/workdir/nodes.mjs +2 -1
- package/dist/workdir/overlay.mjs +2 -2
- package/dist/workdir/sqlite-backend.d.mts +0 -1
- package/dist/workdir/sqlite-backend.mjs +48 -45
- package/dist/workdir/workdir-path.mjs +1 -1
- package/dist/workdir/workdir.mjs +7 -21
- package/dist/workdir/write-tree.mjs +144 -104
- package/package.json +1 -1
- package/dist/workdir/dirty-dir-plan.mjs +0 -73
- package/dist/workdir/dirty-dir.mjs +0 -32
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import { writeObject } from "../objects/raw.mjs";
|
|
2
|
+
import { originBackedNodeId } from "./ids.mjs";
|
|
2
3
|
import { readRepoTree } from "./origin.mjs";
|
|
3
|
-
import {
|
|
4
|
-
import { createNamedOriginChildLookup,
|
|
5
|
-
import { materializeDirtyDirSummary } from "./dirty-dir.mjs";
|
|
4
|
+
import { joinChildPath } from "./workdir-path.mjs";
|
|
5
|
+
import { createNamedOriginChildLookup, resolveNamedChild } from "./directory-view.mjs";
|
|
6
6
|
//#region src/workdir/write-tree.ts
|
|
7
7
|
/**
|
|
8
8
|
* Virtual Workdir overlay -> tree 最小化编译
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* 采用 patchTree 式构造:从 change records + overlay 状态收集脏路径,
|
|
11
|
+
* 沿受影响路径定向编译,只重写有变化的目录 tree 对象。
|
|
12
|
+
*
|
|
13
|
+
* 与旧实现的区别:
|
|
14
|
+
* - 不依赖 DirtyDirSummary 预计算,writeFile/delete 等操作不再触发全量遍历
|
|
15
|
+
* - 脏路径收集仅在 writeTree 时按需执行
|
|
16
|
+
* - 已解析节点中的 overlay 修改通过定向遍历发现
|
|
12
17
|
*
|
|
13
18
|
* writeTree() 成功后不清空 overlay,不推进 baseTree。
|
|
14
19
|
*/
|
|
@@ -30,131 +35,166 @@ import { materializeDirtyDirSummary } from "./dirty-dir.mjs";
|
|
|
30
35
|
function writeTreeFromSession(source, state) {
|
|
31
36
|
const root = state.getNode("root");
|
|
32
37
|
if (root === null || root.state.kind !== "directory") throw new Error("Virtual workdir: root node is missing or not a directory");
|
|
33
|
-
|
|
38
|
+
const changes = /* @__PURE__ */ new Map();
|
|
39
|
+
for (const record of state.listChangeRecords()) changes.set(record.path, record);
|
|
40
|
+
return compileDirectory({
|
|
41
|
+
writeSource: source,
|
|
42
|
+
readSource: source,
|
|
43
|
+
state,
|
|
44
|
+
changes,
|
|
45
|
+
dirtyPaths: collectDirtyPaths(source, state, changes)
|
|
46
|
+
}, root, "");
|
|
34
47
|
}
|
|
35
48
|
/**
|
|
36
|
-
*
|
|
49
|
+
* 收集所有需要编译的目录路径。
|
|
37
50
|
*
|
|
38
|
-
*
|
|
51
|
+
* 来源:
|
|
52
|
+
* 1. Change records 中每条路径及其祖先
|
|
53
|
+
* 2. 已解析节点中有 overlay 修改的目录及其祖先
|
|
39
54
|
*/
|
|
40
|
-
function
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
if (
|
|
45
|
-
|
|
46
|
-
const newEntries = collectCompiledEntries(writeSource, readSource, state, dirNode, dirPath, summary, (changed) => {
|
|
47
|
-
if (changed) anyChanged = true;
|
|
48
|
-
});
|
|
49
|
-
if (summary !== null && (summary.dirtyEntryCount > 0 || summary.dirtyDescendantCount > 0)) anyChanged = true;
|
|
50
|
-
if (!anyChanged && dirNode.origin.kind === "repo-tree") {
|
|
51
|
-
state.setDirtyDirSummary(materializeDirtyDirSummary(summary, dirPath, dirNode.origin.hash));
|
|
52
|
-
return dirNode.origin.hash;
|
|
53
|
-
}
|
|
54
|
-
newEntries.sort((a, b) => a.name.localeCompare(b.name));
|
|
55
|
-
const treeHash = writeObject(writeSource, {
|
|
56
|
-
type: "tree",
|
|
57
|
-
entries: newEntries
|
|
58
|
-
});
|
|
59
|
-
state.setDirtyDirSummary(materializeDirtyDirSummary(summary, dirPath, treeHash));
|
|
60
|
-
return treeHash;
|
|
55
|
+
function collectDirtyPaths(source, state, changes) {
|
|
56
|
+
const dirty = /* @__PURE__ */ new Set();
|
|
57
|
+
for (const path of changes.keys()) addPathAndAncestors(dirty, path);
|
|
58
|
+
const root = state.getNode("root");
|
|
59
|
+
if (root !== null && root.state.kind === "directory") walkResolvedOverlayNodes(source, state, root, "", dirty);
|
|
60
|
+
return dirty;
|
|
61
61
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
return [entry.entry];
|
|
71
|
-
});
|
|
72
|
-
const originLookup = createNamedOriginChildLookup(readRepoTree(readSource, dirNode.origin.hash, dirPath).entries);
|
|
73
|
-
const childPlan = planAffectedDirectoryChildren(originLookup, collectAffectedChildNames(summary));
|
|
74
|
-
const out = [];
|
|
75
|
-
for (const planEntry of childPlan) {
|
|
76
|
-
if (!planEntry.shouldCompile) {
|
|
77
|
-
if (planEntry.originEntry === null) throw new Error(`collectCompiledEntries: missing origin entry for '${planEntry.name}'`);
|
|
78
|
-
out.push(planEntry.originEntry);
|
|
79
|
-
continue;
|
|
62
|
+
/** 将路径及其所有祖先加入集合 */
|
|
63
|
+
function addPathAndAncestors(dirty, path) {
|
|
64
|
+
dirty.add(path);
|
|
65
|
+
while (true) {
|
|
66
|
+
const slashIndex = path.lastIndexOf("/");
|
|
67
|
+
if (slashIndex < 0) {
|
|
68
|
+
if (path !== "") dirty.add("");
|
|
69
|
+
return;
|
|
80
70
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
markChanged(compiled.changed);
|
|
84
|
-
out.push(compiled.entry);
|
|
71
|
+
path = path.slice(0, slashIndex);
|
|
72
|
+
dirty.add(path);
|
|
85
73
|
}
|
|
86
|
-
return out;
|
|
87
74
|
}
|
|
88
|
-
|
|
89
|
-
|
|
75
|
+
/**
|
|
76
|
+
* 沿已解析节点遍历,将存在 overlay 修改的目录路径加入脏路径集。
|
|
77
|
+
*
|
|
78
|
+
* 仅遍历已在 state 中解析的节点(origin 子节点通过 ensureNodeFromTreeEntry
|
|
79
|
+
* 懒注册时才会在 state 中存在),未解析的子树跳过。
|
|
80
|
+
*/
|
|
81
|
+
function walkResolvedOverlayNodes(source, state, node, dirPath, dirty) {
|
|
82
|
+
if (node.state.kind !== "directory") return;
|
|
83
|
+
if (isNodeOverlayDirty(node)) addPathAndAncestors(dirty, dirPath);
|
|
84
|
+
if (node.origin.kind === "repo-tree") {
|
|
85
|
+
const tree = readRepoTree(source, node.origin.hash, dirPath);
|
|
86
|
+
for (const entry of tree.entries) {
|
|
87
|
+
if (entry.mode !== "040000") continue;
|
|
88
|
+
const childId = originBackedNodeId(entry.hash);
|
|
89
|
+
const childNode = state.getNode(childId);
|
|
90
|
+
if (childNode === null) continue;
|
|
91
|
+
walkResolvedOverlayNodes(source, state, childNode, joinChildPath(dirPath, entry.name), dirty);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
for (const [name, childId] of node.state.overlay.addedEntries) {
|
|
95
|
+
const childNode = state.getNode(childId);
|
|
96
|
+
if (childNode === null || childNode.state.kind !== "directory") continue;
|
|
97
|
+
walkResolvedOverlayNodes(source, state, childNode, joinChildPath(dirPath, name), dirty);
|
|
98
|
+
}
|
|
90
99
|
}
|
|
91
|
-
function
|
|
92
|
-
if (
|
|
93
|
-
|
|
94
|
-
if (observedChild === null) return null;
|
|
95
|
-
return compileChildEntry(writeSource, readSource, state, observedChild);
|
|
100
|
+
function isNodeOverlayDirty(node) {
|
|
101
|
+
if (node.state.kind !== "directory") return false;
|
|
102
|
+
return node.state.overlay.addedEntries.size > 0 || node.state.overlay.deletedNames.size > 0;
|
|
96
103
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
104
|
+
/**
|
|
105
|
+
* 递归编译目录 -> tree 对象
|
|
106
|
+
*
|
|
107
|
+
* 返回新 tree 的 SHA-1(无变化时直接复用 origin hash)。
|
|
108
|
+
*/
|
|
109
|
+
function compileDirectory(ctx, dirNode, dirPath) {
|
|
110
|
+
if (dirNode.state.kind !== "directory") throw new Error("compileDirectory called on non-directory node");
|
|
111
|
+
if (!ctx.dirtyPaths.has(dirPath) && !isNodeOverlayDirty(dirNode) && dirNode.origin.kind === "repo-tree") return dirNode.origin.hash;
|
|
112
|
+
let originEntries = [];
|
|
113
|
+
if (dirNode.origin.kind === "repo-tree") originEntries = readRepoTree(ctx.readSource, dirNode.origin.hash, dirPath).entries;
|
|
114
|
+
const overlay = dirNode.state.overlay;
|
|
115
|
+
const treeEntries = [];
|
|
116
|
+
if (dirNode.origin.kind === "repo-tree") {
|
|
117
|
+
const lookup = createNamedOriginChildLookup(originEntries);
|
|
118
|
+
for (const originEntry of originEntries) {
|
|
119
|
+
if (overlay.deletedNames.has(originEntry.name)) continue;
|
|
120
|
+
if (overlay.addedEntries.has(originEntry.name)) continue;
|
|
121
|
+
const childPath = joinChildPath(dirPath, originEntry.name);
|
|
122
|
+
if (originEntry.mode === "040000") {
|
|
123
|
+
const resolved = resolveNamedChild(ctx.state, dirNode, lookup, originEntry.name);
|
|
124
|
+
if (resolved.found && resolved.node.state.kind === "directory") if (ctx.dirtyPaths.has(childPath) || isNodeOverlayDirty(resolved.node)) {
|
|
125
|
+
const hash = compileDirectory(ctx, resolved.node, childPath);
|
|
126
|
+
treeEntries.push({
|
|
127
|
+
mode: "040000",
|
|
128
|
+
name: originEntry.name,
|
|
129
|
+
hash
|
|
130
|
+
});
|
|
131
|
+
} else treeEntries.push(originEntry);
|
|
132
|
+
else if (resolved.found) {
|
|
133
|
+
const compiled = compileNodeToEntry(ctx, resolved.node, childPath, originEntry.name);
|
|
134
|
+
if (compiled !== null) treeEntries.push(compiled);
|
|
135
|
+
} else treeEntries.push(originEntry);
|
|
136
|
+
} else if (ctx.changes.has(childPath)) {
|
|
137
|
+
const resolved = resolveNamedChild(ctx.state, dirNode, lookup, originEntry.name);
|
|
138
|
+
if (resolved.found) {
|
|
139
|
+
const compiled = compileNodeToEntry(ctx, resolved.node, childPath, originEntry.name);
|
|
140
|
+
if (compiled !== null) treeEntries.push(compiled);
|
|
141
|
+
}
|
|
142
|
+
} else treeEntries.push(originEntry);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
for (const [name, nodeId] of overlay.addedEntries) {
|
|
146
|
+
const childNode = ctx.state.getNode(nodeId);
|
|
147
|
+
if (childNode === null) continue;
|
|
148
|
+
const compiled = compileNodeToEntry(ctx, childNode, joinChildPath(dirPath, name), name);
|
|
149
|
+
if (compiled !== null) treeEntries.push(compiled);
|
|
110
150
|
}
|
|
151
|
+
treeEntries.sort((a, b) => a.name.localeCompare(b.name));
|
|
152
|
+
return writeObject(ctx.writeSource, {
|
|
153
|
+
type: "tree",
|
|
154
|
+
entries: treeEntries
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
function compileNodeToEntry(ctx, node, childPath, childName) {
|
|
158
|
+
if (node.state.kind === "directory") return {
|
|
159
|
+
mode: "040000",
|
|
160
|
+
name: childName,
|
|
161
|
+
hash: compileDirectory(ctx, node, childPath)
|
|
162
|
+
};
|
|
111
163
|
if (node.state.kind === "file") {
|
|
112
164
|
if (node.state.content !== void 0) {
|
|
113
|
-
const hash = writeObject(writeSource, {
|
|
165
|
+
const hash = writeObject(ctx.writeSource, {
|
|
114
166
|
type: "blob",
|
|
115
167
|
content: node.state.content
|
|
116
168
|
});
|
|
117
169
|
return {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
hash
|
|
122
|
-
},
|
|
123
|
-
changed: node.origin.kind !== "repo-blob" || hash !== node.origin.hash
|
|
170
|
+
mode: node.state.mode,
|
|
171
|
+
name: childName,
|
|
172
|
+
hash
|
|
124
173
|
};
|
|
125
174
|
}
|
|
126
175
|
if (node.origin.kind === "repo-blob") return {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
hash: node.origin.hash
|
|
131
|
-
},
|
|
132
|
-
changed: false
|
|
176
|
+
mode: node.state.mode,
|
|
177
|
+
name: childName,
|
|
178
|
+
hash: node.origin.hash
|
|
133
179
|
};
|
|
134
180
|
return null;
|
|
135
181
|
}
|
|
136
|
-
if (node.state.
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
name: child.name,
|
|
145
|
-
hash
|
|
146
|
-
},
|
|
147
|
-
changed: node.origin.kind !== "repo-blob" || hash !== node.origin.hash
|
|
182
|
+
if (node.state.kind === "symlink") {
|
|
183
|
+
if (node.state.target !== void 0) return {
|
|
184
|
+
mode: "120000",
|
|
185
|
+
name: childName,
|
|
186
|
+
hash: writeObject(ctx.writeSource, {
|
|
187
|
+
type: "blob",
|
|
188
|
+
content: node.state.target
|
|
189
|
+
})
|
|
148
190
|
};
|
|
149
|
-
|
|
150
|
-
if (node.origin.kind === "repo-blob") return {
|
|
151
|
-
entry: {
|
|
191
|
+
if (node.origin.kind === "repo-blob") return {
|
|
152
192
|
mode: "120000",
|
|
153
|
-
name:
|
|
193
|
+
name: childName,
|
|
154
194
|
hash: node.origin.hash
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
}
|
|
195
|
+
};
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
158
198
|
return null;
|
|
159
199
|
}
|
|
160
200
|
//#endregion
|
package/package.json
CHANGED
|
@@ -1,73 +0,0 @@
|
|
|
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 };
|
|
@@ -1,32 +0,0 @@
|
|
|
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 };
|