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.
@@ -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 { listDirectoryChildren } from "./workdir-path.mjs";
4
- import { createNamedOriginChildLookup, observeListedDirectoryChild, observeNamedDirectoryChild, planAffectedDirectoryChildren } from "./directory-view.mjs";
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
- * 遍历 workdir 的目录 overlay,将受影响的目录重写为新 tree,
11
- * 未修改的 repo-backed 子树/文件尽量复用原对象哈希。
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
- return compileDirectory(source, source, state, root);
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
- * 递归编译目录 overlay -> 新 tree
49
+ * 收集所有需要编译的目录路径。
37
50
  *
38
- * 返回新 tree 的 SHA-1(若目录无任何变化则直接复用 origin hash)。
51
+ * 来源:
52
+ * 1. Change records 中每条路径及其祖先
53
+ * 2. 已解析节点中有 overlay 修改的目录及其祖先
39
54
  */
40
- function compileDirectory(writeSource, readSource, state, dirNode, dirPath = "") {
41
- if (dirNode.state.kind !== "directory") throw new Error("compileDirectory called on non-directory node");
42
- const summary = state.getDirtyDirSummary(dirPath);
43
- if (summary !== null && summary.hashState === "materialized" && summary.currentTreeHash !== null) return summary.currentTreeHash;
44
- if (summary === null && dirNode.origin.kind === "repo-tree") return dirNode.origin.hash;
45
- let anyChanged = false;
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
- function collectCompiledEntries(writeSource, readSource, state, dirNode, dirPath, summary, markChanged) {
63
- if (dirNode.state.kind !== "directory") throw new Error("collectCompiledEntries called on non-directory node");
64
- if (dirNode.origin.kind !== "repo-tree" || summary === null) return listDirectoryChildren(readSource, state, dirNode, dirPath).flatMap((child) => {
65
- const observedChild = observeListedDirectoryChild(state, dirPath, child);
66
- if (observedChild === null) return [];
67
- const entry = compileChildEntry(writeSource, readSource, state, observedChild);
68
- if (entry === null) return [];
69
- markChanged(entry.changed);
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
- const compiled = compileNamedChildEntry(writeSource, readSource, state, dirNode, dirPath, planEntry.name, originLookup);
82
- if (compiled === null) continue;
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
- function collectAffectedChildNames(summary) {
89
- return new Set(summary.affectedNames);
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 compileNamedChildEntry(writeSource, readSource, state, dirNode, dirPath, name, originLookup) {
92
- if (dirNode.state.kind !== "directory") throw new Error("compileNamedChildEntry called on non-directory node");
93
- const observedChild = observeNamedDirectoryChild(state, dirNode, dirPath, originLookup, name);
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
- function compileChildEntry(writeSource, readSource, state, child) {
98
- const node = child.node;
99
- if (node.state.kind === "directory") {
100
- const newHash = compileDirectory(writeSource, readSource, state, node, child.path);
101
- const originHash = node.origin.kind === "repo-tree" ? node.origin.hash : null;
102
- return {
103
- entry: {
104
- mode: "040000",
105
- name: child.name,
106
- hash: newHash
107
- },
108
- changed: newHash !== originHash
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
- entry: {
119
- mode: node.state.mode,
120
- name: child.name,
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
- entry: {
128
- mode: node.state.mode,
129
- name: child.name,
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.target !== void 0) {
137
- const hash = writeObject(writeSource, {
138
- type: "blob",
139
- content: node.state.target
140
- });
141
- return {
142
- entry: {
143
- mode: "120000",
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: child.name,
193
+ name: childName,
154
194
  hash: node.origin.hash
155
- },
156
- changed: false
157
- };
195
+ };
196
+ return null;
197
+ }
158
198
  return null;
159
199
  }
160
200
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nano-git",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
4
4
  "files": [
5
5
  "dist"
6
6
  ],
@@ -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 };