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,15 +1,6 @@
|
|
|
1
|
-
import {
|
|
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,
|
|
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 {
|
package/dist/workdir/nodes.mjs
CHANGED
|
@@ -61,7 +61,8 @@ function revertNodeState(node) {
|
|
|
61
61
|
};
|
|
62
62
|
}
|
|
63
63
|
/**
|
|
64
|
-
* 为 `copy` 创建新节点:共享 origin
|
|
64
|
+
* 为 `copy` 创建新节点:共享 origin,目录采用 CoW(写时复制)
|
|
65
|
+
* (子项绑定保留,但 nodeId 为新;实际子树只在任一副本写入时分裂)
|
|
65
66
|
*/
|
|
66
67
|
function cloneWorkdirNodeForCopy(source, newId) {
|
|
67
68
|
const origin = source.origin;
|
package/dist/workdir/overlay.mjs
CHANGED
|
@@ -58,7 +58,7 @@ function mergeDirectoryChildren(originChildren, overlay, addedEntryModes) {
|
|
|
58
58
|
return merged;
|
|
59
59
|
}
|
|
60
60
|
/**
|
|
61
|
-
* 在目录 overlay 中绑定或覆盖条目(create / modify /
|
|
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
|
-
*
|
|
87
|
+
* 在同一目录内 move:复用 nodeId,仅改绑定名
|
|
88
88
|
*/
|
|
89
89
|
function overlayRenameEntry(overlay, fromName, toName, nodeId) {
|
|
90
90
|
let next = overlayTombstoneEntry(overlay, fromName);
|
|
@@ -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
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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
|
|
53
|
+
const conn = acquireConnection(dbPath, options.walMode !== false);
|
|
53
54
|
try {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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(
|
|
72
|
-
const transactImpl = db.transaction((fn) => fn());
|
|
73
|
-
const readBaseTreeStmt =
|
|
74
|
-
const upsertWorkdirStmt =
|
|
75
|
-
const getNodeStmt =
|
|
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 =
|
|
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 =
|
|
92
|
-
const clearNodesStmt =
|
|
93
|
-
const listChangesStmt =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
114
|
-
const clearChangesStmt =
|
|
115
|
-
const listDirtyDirsStmt =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
135
|
-
const clearDirtyDirsStmt =
|
|
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 === "
|
|
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) {
|
package/dist/workdir/workdir.mjs
CHANGED
|
@@ -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:完整文件/目录
|
|
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
|
-
|
|
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
|
|
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
|
};
|