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,15 +1,6 @@
1
- import { VirtualNotDirectoryError } from "../core/errors.mjs";
2
- import { ensureNodeFromTreeEntry, joinChildPath, listDirectoryChildren } from "./workdir-path.mjs";
1
+ import { ensureNodeFromTreeEntry, joinChildPath } from "./workdir-path.mjs";
3
2
  //#region src/workdir/directory-view.ts
4
3
  /**
5
- * Virtual Workdir 目录展开、观察与编译计划
6
- *
7
- * 从 workdir-path.ts 拆分,聚焦以下职责:
8
- * - 目录子项观察(observeDirectoryChildren / observeListedDirectoryChild / observeNamedDirectoryChild)
9
- * - origin 按名查询视图(createNamedOriginChildLookup)
10
- * - 受影响子项编译计划(planAffectedDirectoryChildren)
11
- */
12
- /**
13
4
  * 在不展开整个目录列表的前提下,按名称定向解析单个子节点。
14
5
  *
15
6
  * 优先读取 overlay 绑定;若 overlay 未覆盖,再按需从 origin 条目懒注册。
@@ -71,78 +62,6 @@ function observeListedDirectoryChild(state, dirPath, child) {
71
62
  };
72
63
  }
73
64
  /**
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
65
  * 为目录 origin 条目创建按名查询视图。
147
66
  *
148
67
  * @example
@@ -163,35 +82,5 @@ function createNamedOriginChildLookup(entries) {
163
82
  }
164
83
  };
165
84
  }
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
85
  //#endregion
197
- export { createNamedOriginChildLookup, observeDirectoryChildren, observeListedDirectoryChild, observeNamedDirectoryChild, planAffectedDirectoryChildren };
86
+ export { createNamedOriginChildLookup, observeListedDirectoryChild, resolveNamedChild };
@@ -372,8 +372,19 @@ function restoreChangeRecord(record) {
372
372
  ...record.current,
373
373
  hash: record.current.hash
374
374
  },
375
- source: record.source
375
+ source: record.source === null ? null : readFileDiffSource(record.source)
376
+ };
377
+ }
378
+ function readFileDiffSource(source) {
379
+ if (source.kind === "copy") return {
380
+ kind: "copy",
381
+ path: source.path
382
+ };
383
+ if (source.kind === "move" || source.kind === "rename") return {
384
+ kind: "move",
385
+ path: source.path
376
386
  };
387
+ throw new Error(`Invalid file workdir diff source kind: ${String(source.kind)}`);
377
388
  }
378
389
  function serializeDirtyDirSummary(summary) {
379
390
  return {
@@ -61,7 +61,8 @@ function revertNodeState(node) {
61
61
  };
62
62
  }
63
63
  /**
64
- * 为 `copy` 创建新节点:共享 origin,目录为浅复制(子项绑定保留,但 nodeId 为新)
64
+ * 为 `copy` 创建新节点:共享 origin,目录采用 CoW(写时复制)
65
+ * (子项绑定保留,但 nodeId 为新;实际子树只在任一副本写入时分裂)
65
66
  */
66
67
  function cloneWorkdirNodeForCopy(source, newId) {
67
68
  const origin = source.origin;
@@ -58,7 +58,7 @@ function mergeDirectoryChildren(originChildren, overlay, addedEntryModes) {
58
58
  return merged;
59
59
  }
60
60
  /**
61
- * 在目录 overlay 中绑定或覆盖条目(create / modify / rename 目标 / copy 目标)
61
+ * 在目录 overlay 中绑定或覆盖条目(create / modify / move 目标 / copy 目标)
62
62
  */
63
63
  function overlayBindEntry(overlay, name, nodeId) {
64
64
  const addedEntries = new Map(overlay.addedEntries);
@@ -84,7 +84,7 @@ function overlayTombstoneEntry(overlay, name) {
84
84
  };
85
85
  }
86
86
  /**
87
- * 在同一目录内重命名:复用 nodeId,仅改绑定名
87
+ * 在同一目录内 move:复用 nodeId,仅改绑定名
88
88
  */
89
89
  function overlayRenameEntry(overlay, fromName, toName, nodeId) {
90
90
  let next = overlayTombstoneEntry(overlay, fromName);
@@ -1,6 +1,5 @@
1
1
  import { ObjectDatabase } from "../core/types/odb.mjs";
2
2
  import { CreateVirtualWorkdirOptions, VirtualWorkdir } from "./core.mjs";
3
- import { Database } from "bun:sqlite";
4
3
 
5
4
  //#region src/workdir/sqlite-backend.d.ts
6
5
  /** SQLite 连接层的可选参数 */
@@ -1,7 +1,7 @@
1
1
  import { sha1 } from "../core/types.mjs";
2
+ import { acquireConnection } from "../backend/sqlite-pool.mjs";
2
3
  import { createRootDirectoryNode } from "./nodes.mjs";
3
4
  import { openVirtualWorkdir } from "./workdir.mjs";
4
- import { Database } from "bun:sqlite";
5
5
  //#region src/workdir/sqlite-backend.ts
6
6
  /**
7
7
  * Virtual Workdir SQLite backend
@@ -20,25 +20,26 @@ const WORKDIR_SQLITE_SCHEMA_VERSION = 6;
20
20
  * ```
21
21
  */
22
22
  function openSqliteVirtualWorkdir(source, dbPath, workdirKey, options) {
23
- const db = new Database(dbPath);
24
- if (options.walMode !== false) db.run("PRAGMA journal_mode = WAL");
25
- ensureSchema(db);
26
- const store = createSqliteVirtualWorkdirStateStore(db, workdirKey);
27
- if (!hasWorkdir(db, workdirKey)) {
28
- if (options.create !== true) {
29
- db.close();
30
- throw new Error(`Virtual workdir not found: ${workdirKey}`);
23
+ const conn = acquireConnection(dbPath, options.walMode !== false);
24
+ try {
25
+ ensureSchema(conn.db);
26
+ const store = createSqliteVirtualWorkdirStateStore(conn, workdirKey);
27
+ if (!hasWorkdir(conn.db, workdirKey)) {
28
+ if (options.create !== true) throw new Error(`Virtual workdir not found: ${workdirKey}`);
29
+ store.reset(options.baseTree);
31
30
  }
32
- store.reset(options.baseTree);
31
+ validateWorkdirIntegrity(conn.db, workdirKey);
32
+ let released = false;
33
+ const workdir = openVirtualWorkdir(source, store);
34
+ return Object.assign(workdir, { [Symbol.dispose]() {
35
+ if (released) return;
36
+ released = true;
37
+ conn.release();
38
+ } });
39
+ } catch (error) {
40
+ conn.release();
41
+ throw error;
33
42
  }
34
- validateWorkdirIntegrity(db, workdirKey);
35
- let disposed = false;
36
- const workdir = openVirtualWorkdir(source, store);
37
- return Object.assign(workdir, { [Symbol.dispose]() {
38
- if (disposed) return;
39
- disposed = true;
40
- db.close();
41
- } });
42
43
  }
43
44
  /**
44
45
  * 删除指定 key 上的 SQLite VirtualWorkdir
@@ -49,14 +50,13 @@ function openSqliteVirtualWorkdir(source, dbPath, workdirKey, options) {
49
50
  * ```
50
51
  */
51
52
  function deleteSqliteVirtualWorkdir(dbPath, workdirKey, options = {}) {
52
- const db = new Database(dbPath);
53
+ const conn = acquireConnection(dbPath, options.walMode !== false);
53
54
  try {
54
- if (options.walMode !== false) db.run("PRAGMA journal_mode = WAL");
55
- ensureSchema(db);
56
- if (!hasWorkdir(db, workdirKey)) throw new Error(`Virtual workdir not found: ${workdirKey}`);
57
- deleteWorkdirRows(db, workdirKey);
55
+ ensureSchema(conn.db);
56
+ if (!hasWorkdir(conn.db, workdirKey)) throw new Error(`Virtual workdir not found: ${workdirKey}`);
57
+ deleteWorkdirRows(conn.db, workdirKey);
58
58
  } finally {
59
- db.close();
59
+ conn.release();
60
60
  }
61
61
  }
62
62
  /**
@@ -64,18 +64,19 @@ function deleteSqliteVirtualWorkdir(dbPath, workdirKey, options = {}) {
64
64
  *
65
65
  * @example
66
66
  * ```ts
67
- * const store = createSqliteVirtualWorkdirStateStore(db, "demo");
67
+ * using conn = acquireConnection("/tmp/workdir.sqlite");
68
+ * const store = createSqliteVirtualWorkdirStateStore(conn, "demo");
68
69
  * expect(store.kind).toBe("sqlite");
69
70
  * ```
70
71
  */
71
- function createSqliteVirtualWorkdirStateStore(db, workdirKey) {
72
- const transactImpl = db.transaction((fn) => fn());
73
- const readBaseTreeStmt = db.query("SELECT base_tree FROM workdirs WHERE workdir_key = ?");
74
- const upsertWorkdirStmt = db.query("INSERT INTO workdirs (workdir_key, base_tree) VALUES (?, ?) ON CONFLICT(workdir_key) DO UPDATE SET base_tree = excluded.base_tree");
75
- const getNodeStmt = db.query(`SELECT node_id, origin_kind, origin_hash, origin_mode, state_kind, state_mode, content, target, directory_overlay
72
+ function createSqliteVirtualWorkdirStateStore(conn, workdirKey) {
73
+ const transactImpl = conn.db.transaction((fn) => fn());
74
+ const readBaseTreeStmt = conn.prepare("SELECT base_tree FROM workdirs WHERE workdir_key = ?");
75
+ const upsertWorkdirStmt = conn.prepare("INSERT INTO workdirs (workdir_key, base_tree) VALUES (?, ?) ON CONFLICT(workdir_key) DO UPDATE SET base_tree = excluded.base_tree");
76
+ const getNodeStmt = conn.prepare(`SELECT node_id, origin_kind, origin_hash, origin_mode, state_kind, state_mode, content, target, directory_overlay
76
77
  FROM workdir_nodes
77
78
  WHERE workdir_key = ? AND node_id = ?`);
78
- const setNodeStmt = db.query(`INSERT INTO workdir_nodes (
79
+ const setNodeStmt = conn.prepare(`INSERT INTO workdir_nodes (
79
80
  workdir_key, node_id, origin_kind, origin_hash, origin_mode,
80
81
  state_kind, state_mode, content, target, directory_overlay
81
82
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
@@ -88,16 +89,16 @@ function createSqliteVirtualWorkdirStateStore(db, workdirKey) {
88
89
  content = excluded.content,
89
90
  target = excluded.target,
90
91
  directory_overlay = excluded.directory_overlay`);
91
- const deleteNodeStmt = db.query("DELETE FROM workdir_nodes WHERE workdir_key = ? AND node_id = ?");
92
- const clearNodesStmt = db.query("DELETE FROM workdir_nodes WHERE workdir_key = ?");
93
- const listChangesStmt = db.query(`SELECT path, previous_kind, previous_mode, previous_hash, current_kind, current_mode, current_hash, source_kind, source_path
92
+ const deleteNodeStmt = conn.prepare("DELETE FROM workdir_nodes WHERE workdir_key = ? AND node_id = ?");
93
+ const clearNodesStmt = conn.prepare("DELETE FROM workdir_nodes WHERE workdir_key = ?");
94
+ const listChangesStmt = conn.prepare(`SELECT path, previous_kind, previous_mode, previous_hash, current_kind, current_mode, current_hash, source_kind, source_path
94
95
  FROM workdir_changes
95
96
  WHERE workdir_key = ?
96
97
  ORDER BY path`);
97
- const getChangeStmt = db.query(`SELECT path, previous_kind, previous_mode, previous_hash, current_kind, current_mode, current_hash, source_kind, source_path
98
+ const getChangeStmt = conn.prepare(`SELECT path, previous_kind, previous_mode, previous_hash, current_kind, current_mode, current_hash, source_kind, source_path
98
99
  FROM workdir_changes
99
100
  WHERE workdir_key = ? AND path = ?`);
100
- const upsertChangeStmt = db.query(`INSERT INTO workdir_changes (
101
+ const upsertChangeStmt = conn.prepare(`INSERT INTO workdir_changes (
101
102
  workdir_key, path, previous_kind, previous_mode, previous_hash,
102
103
  current_kind, current_mode, current_hash, source_kind, source_path
103
104
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
@@ -110,16 +111,16 @@ function createSqliteVirtualWorkdirStateStore(db, workdirKey) {
110
111
  current_hash = excluded.current_hash,
111
112
  source_kind = excluded.source_kind,
112
113
  source_path = excluded.source_path`);
113
- const deleteChangeStmt = db.query("DELETE FROM workdir_changes WHERE workdir_key = ? AND path = ?");
114
- const clearChangesStmt = db.query("DELETE FROM workdir_changes WHERE workdir_key = ?");
115
- const listDirtyDirsStmt = db.query(`SELECT path, is_dirty, dirty_entry_count, dirty_descendant_count, affected_names, current_tree_hash, hash_state
114
+ const deleteChangeStmt = conn.prepare("DELETE FROM workdir_changes WHERE workdir_key = ? AND path = ?");
115
+ const clearChangesStmt = conn.prepare("DELETE FROM workdir_changes WHERE workdir_key = ?");
116
+ const listDirtyDirsStmt = conn.prepare(`SELECT path, is_dirty, dirty_entry_count, dirty_descendant_count, affected_names, current_tree_hash, hash_state
116
117
  FROM workdir_dirty_dirs
117
118
  WHERE workdir_key = ?
118
119
  ORDER BY path`);
119
- const getDirtyDirStmt = db.query(`SELECT path, is_dirty, dirty_entry_count, dirty_descendant_count, affected_names, current_tree_hash, hash_state
120
+ const getDirtyDirStmt = conn.prepare(`SELECT path, is_dirty, dirty_entry_count, dirty_descendant_count, affected_names, current_tree_hash, hash_state
120
121
  FROM workdir_dirty_dirs
121
122
  WHERE workdir_key = ? AND path = ?`);
122
- const upsertDirtyDirStmt = db.query(`INSERT INTO workdir_dirty_dirs (
123
+ const upsertDirtyDirStmt = conn.prepare(`INSERT INTO workdir_dirty_dirs (
123
124
  workdir_key, path, is_dirty, dirty_entry_count, dirty_descendant_count,
124
125
  affected_names, current_tree_hash, hash_state
125
126
  )
@@ -131,9 +132,9 @@ function createSqliteVirtualWorkdirStateStore(db, workdirKey) {
131
132
  affected_names = excluded.affected_names,
132
133
  current_tree_hash = excluded.current_tree_hash,
133
134
  hash_state = excluded.hash_state`);
134
- const deleteDirtyDirStmt = db.query("DELETE FROM workdir_dirty_dirs WHERE workdir_key = ? AND path = ?");
135
- const clearDirtyDirsStmt = db.query("DELETE FROM workdir_dirty_dirs WHERE workdir_key = ?");
136
- const resetTx = db.transaction((baseTree) => {
135
+ const deleteDirtyDirStmt = conn.prepare("DELETE FROM workdir_dirty_dirs WHERE workdir_key = ? AND path = ?");
136
+ const clearDirtyDirsStmt = conn.prepare("DELETE FROM workdir_dirty_dirs WHERE workdir_key = ?");
137
+ const resetTx = conn.db.transaction((baseTree) => {
137
138
  upsertWorkdirStmt.run(workdirKey, baseTree);
138
139
  clearNodesStmt.run(workdirKey);
139
140
  clearChangesStmt.run(workdirKey);
@@ -381,8 +382,10 @@ function readDiffObjectMode(raw) {
381
382
  if (raw === "100644" || raw === "100755" || raw === "120000") return raw;
382
383
  throw new Error(`Invalid SQLite workdir diff object mode: ${raw}`);
383
384
  }
385
+ /** 解析 diff 来源种类;旧版 `rename` 读入时规范为 `move`。 */
384
386
  function readDiffSourceKind(raw) {
385
- if (raw === "rename" || raw === "copy") return raw;
387
+ if (raw === "move" || raw === "copy") return raw;
388
+ if (raw === "rename") return "move";
386
389
  throw new Error(`Invalid SQLite workdir diff source kind: ${raw}`);
387
390
  }
388
391
  function readDirtyDirSummary(row) {
@@ -180,7 +180,7 @@ function resolveCurrentLeafAtPath(source, state, path) {
180
180
  };
181
181
  }
182
182
  /**
183
- * 解析 `rename` / `copy` 这类“已存在源 -> 目标路径”的双路径上下文。
183
+ * 解析 `move` / `copy` 这类“已存在源 -> 目标路径”的双路径上下文。
184
184
  *
185
185
  * @example
186
186
  * ```ts
@@ -2,11 +2,10 @@ import { VirtualNotDirectoryError, VirtualNotFileError, VirtualNotSymlinkError,
2
2
  import { createNodeId } from "./ids.mjs";
3
3
  import { modeToVirtualEntryKind, readRepoBlobContent } from "./origin.mjs";
4
4
  import { overlayBindEntry, overlayRenameEntry, overlayTombstoneEntry } from "./overlay.mjs";
5
- import { assertValidVirtualPath, normalizeDirectoryPath, splitPathSegments } from "./path.mjs";
5
+ import { assertValidVirtualPath, normalizeDirectoryPath, parentPath, splitPathSegments } from "./path.mjs";
6
6
  import { getDirectoryChildrenView, getRootNode, requireExistingWriteTarget, requireMissingWriteTarget, resolveLeafWriteTarget, resolvePath, resolveWriteTransfer } from "./workdir-path.mjs";
7
7
  import { createChangeIndexPlanner } from "./change-index-plan.mjs";
8
8
  import { computeVirtualDiff, rebuildNormalizedChangeIndex, refreshChangeRecordForPath, replaceChangeRecords, rewriteChangeRecordForRename, writeChangeRecordForCopy } from "./change-index.mjs";
9
- import { createDirtyDirPlanner } from "./dirty-dir-plan.mjs";
10
9
  import { revertNodeState } from "./nodes.mjs";
11
10
  import { createVirtualWorkdirMemoryStateStore } from "./memory-backend.mjs";
12
11
  import { cloneNodeGraphForCopy, runInWriteTransaction, statDirectoryNode, statNode, updateParentOverlay } from "./workdir-transaction.mjs";
@@ -15,7 +14,7 @@ import { writeTreeFromSession } from "./write-tree.mjs";
15
14
  /**
16
15
  * VirtualWorkdir 行为编排
17
16
  *
18
- * Phase 5:完整文件/目录 rename 与 copy 语义。
17
+ * Phase 5:完整文件/目录 move 与 copy 语义。
19
18
  */
20
19
  /**
21
20
  * 基于 ObjectDatabase 创建 VirtualWorkdir
@@ -88,7 +87,6 @@ function openVirtualWorkdir(source, state) {
88
87
  rewriteRename: rewriteChangeIndexForRename,
89
88
  writeCopy: writeChangeIndexForCopy
90
89
  });
91
- const dirtyDirPlanner = createDirtyDirPlanner(source, state);
92
90
  refreshChangeIndex();
93
91
  const createDirectoryAtPath = (path) => {
94
92
  const target = requireMissingWriteTarget(source, state, path);
@@ -172,14 +170,7 @@ function openVirtualWorkdir(source, state) {
172
170
  },
173
171
  mkdir(path, options) {
174
172
  const recursive = options?.recursive === true;
175
- runInWriteTransaction(state, () => {
176
- if (recursive) {
177
- const segments = splitPathSegments(path);
178
- const paths = [];
179
- for (let i = 0; i < segments.length; i++) paths.push(segments.slice(0, i + 1).join("/"));
180
- dirtyDirPlanner.rebuild(paths);
181
- } else dirtyDirPlanner.rebuild([path]);
182
- }, invalidateDiffCaches, () => {
173
+ runInWriteTransaction(state, null, invalidateDiffCaches, () => {
183
174
  if (recursive) mkdirRecursive(path);
184
175
  else createDirectoryAtPath(path);
185
176
  });
@@ -187,7 +178,6 @@ function openVirtualWorkdir(source, state) {
187
178
  writeFile(path, content, options) {
188
179
  runInWriteTransaction(state, () => {
189
180
  changeIndexPlanner.apply(changeIndexPlanner.planRefreshForPath(path));
190
- dirtyDirPlanner.rebuild([path]);
191
181
  }, invalidateDiffCaches, () => {
192
182
  const mode = options?.mode ?? "100644";
193
183
  const target = resolveLeafWriteTarget(source, state, path);
@@ -208,7 +198,6 @@ function openVirtualWorkdir(source, state) {
208
198
  writeLink(path, target) {
209
199
  runInWriteTransaction(state, () => {
210
200
  changeIndexPlanner.apply(changeIndexPlanner.planRefreshForPath(path));
211
- dirtyDirPlanner.rebuild([path]);
212
201
  }, invalidateDiffCaches, () => {
213
202
  const writeTarget = resolveLeafWriteTarget(source, state, path);
214
203
  const nodeId = writeTarget.existing !== null ? writeTarget.existing.node.id : createNodeId();
@@ -231,27 +220,27 @@ function openVirtualWorkdir(source, state) {
231
220
  }
232
221
  runInWriteTransaction(state, () => {
233
222
  changeIndexPlanner.apply(changeIndexPlanner.planRefreshForPath(path, { treatMissingAsIncremental: true }));
234
- dirtyDirPlanner.rebuild([path]);
235
223
  }, invalidateDiffCaches, () => {
236
224
  const target = requireExistingWriteTarget(source, state, path);
237
225
  updateParentOverlay(state, target.parentNode.id, overlayTombstoneEntry(target.parentNode.state.overlay, target.name));
238
226
  });
239
227
  },
240
- rename(from, to) {
228
+ move(from, to) {
241
229
  runInWriteTransaction(state, () => {
242
230
  changeIndexPlanner.apply(changeIndexPlanner.planRewriteForRename(from, to));
243
- dirtyDirPlanner.rebuild([from, to]);
244
231
  }, invalidateDiffCaches, () => {
245
232
  assertValidVirtualPath(from);
246
233
  assertValidVirtualPath(to);
247
234
  if (from === to) return;
235
+ const toParent = parentPath(to);
236
+ if (toParent !== null) mkdirRecursive(toParent);
248
237
  const { from: fromTarget, to: toTarget } = resolveWriteTransfer(source, state, from, to);
249
238
  const sourceNode = fromTarget.existing.node;
250
239
  if (toTarget.existing !== null) throw new VirtualPathAlreadyExistsError(to);
251
240
  if (sourceNode.state.kind === "directory") {
252
241
  const toPath = to;
253
242
  const fromPath = from;
254
- if (toPath.startsWith(fromPath + "/") || toPath === fromPath) throw new Error(`Cannot rename '${from}' to '${to}': destination is a subdirectory of source`);
243
+ if (toPath.startsWith(fromPath + "/") || toPath === fromPath) throw new Error(`Cannot move '${from}' to '${to}': destination is a subdirectory of source`);
255
244
  }
256
245
  if (fromTarget.parentNode.id === toTarget.parentNode.id) updateParentOverlay(state, fromTarget.parentNode.id, overlayRenameEntry(fromTarget.parentNode.state.overlay, fromTarget.name, toTarget.name, sourceNode.id));
257
246
  else {
@@ -263,7 +252,6 @@ function openVirtualWorkdir(source, state) {
263
252
  copy(from, to) {
264
253
  runInWriteTransaction(state, () => {
265
254
  changeIndexPlanner.apply(changeIndexPlanner.planWriteForCopy(from, to));
266
- dirtyDirPlanner.rebuild([from, to]);
267
255
  }, invalidateDiffCaches, () => {
268
256
  assertValidVirtualPath(from);
269
257
  assertValidVirtualPath(to);
@@ -278,7 +266,6 @@ function openVirtualWorkdir(source, state) {
278
266
  revert(path) {
279
267
  runInWriteTransaction(state, () => {
280
268
  changeIndexPlanner.apply(changeIndexPlanner.planRefreshForPath(path));
281
- dirtyDirPlanner.rebuild([path]);
282
269
  }, invalidateDiffCaches, () => {
283
270
  assertValidVirtualPath(path);
284
271
  const resolved = resolvePath(source, state, path);
@@ -298,7 +285,6 @@ function openVirtualWorkdir(source, state) {
298
285
  reset(baseTree) {
299
286
  runInWriteTransaction(state, null, invalidateDiffCaches, () => {
300
287
  state.reset(baseTree);
301
- dirtyDirPlanner.clear();
302
288
  });
303
289
  }
304
290
  };