nano-git 0.3.5 → 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/core.d.mts +2 -1
- package/dist/workdir/directory-view.mjs +2 -113
- package/dist/workdir/nodes.mjs +2 -1
- package/dist/workdir/sqlite-backend.d.mts +0 -1
- package/dist/workdir/sqlite-backend.mjs +45 -44
- package/dist/workdir/workdir.mjs +1 -23
- 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
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Database, Statement } from "bun:sqlite";
|
|
2
|
+
|
|
3
|
+
//#region src/backend/sqlite-pool.d.ts
|
|
4
|
+
/** 连接池获取结果 */
|
|
5
|
+
interface SqliteConnectionHandle {
|
|
6
|
+
readonly db: Database;
|
|
7
|
+
/** 释放连接(引用计数减一,归零时关闭数据库) */
|
|
8
|
+
readonly release: () => void;
|
|
9
|
+
/**
|
|
10
|
+
* 获取缓存的 prepared statement
|
|
11
|
+
*
|
|
12
|
+
* 相同 SQL 文本返回同一实例,避免重复编译。
|
|
13
|
+
* 绑定参数类型由调用方按 SQL 保证一致,此处不做泛型约束。
|
|
14
|
+
*/
|
|
15
|
+
readonly prepare: <T>(sql: string) => Statement<T, any[]>;
|
|
16
|
+
/** 支持 `using` 语法自动释放 */
|
|
17
|
+
[Symbol.dispose](): void;
|
|
18
|
+
}
|
|
19
|
+
//#endregion
|
|
20
|
+
export { SqliteConnectionHandle };
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
//#region src/backend/sqlite-pool.ts
|
|
3
|
+
/**
|
|
4
|
+
* SQLite 连接池(引用计数)
|
|
5
|
+
*
|
|
6
|
+
* 缓存 Database 实例,相同 dbPath 复用同一连接,
|
|
7
|
+
* 防止反复打开同一数据库文件带来的开销。
|
|
8
|
+
*
|
|
9
|
+
* 每个 acquire 对应一个 release,当引用计数归零时自动关闭连接。
|
|
10
|
+
* 返回的 handle 实现了 [Symbol.dispose](),可用 `using` 语法自动释放。
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* using _conn = acquireConnection("/tmp/repo.sqlite");
|
|
15
|
+
* // 作用域结束时自动释放
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
const pool = /* @__PURE__ */ new Map();
|
|
19
|
+
/** 构造连接句柄 */
|
|
20
|
+
function makeHandle(db, stmts, onRelease) {
|
|
21
|
+
return {
|
|
22
|
+
db,
|
|
23
|
+
release: onRelease,
|
|
24
|
+
prepare(sql) {
|
|
25
|
+
let stmt = stmts.get(sql);
|
|
26
|
+
if (stmt === void 0) {
|
|
27
|
+
stmt = db.query(sql);
|
|
28
|
+
stmts.set(sql, stmt);
|
|
29
|
+
}
|
|
30
|
+
return stmt;
|
|
31
|
+
},
|
|
32
|
+
[Symbol.dispose]() {
|
|
33
|
+
onRelease();
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* 从全局连接池获取或创建 SQLite 数据库连接
|
|
39
|
+
*
|
|
40
|
+
* 相同 dbPath 返回同一个 Database 实例,通过引用计数管理生命周期。
|
|
41
|
+
* 首次打开时根据 walMode 执行 PRAGMA journal_mode = WAL。
|
|
42
|
+
* 后续打开(缓存命中)忽略 walMode 参数。
|
|
43
|
+
*
|
|
44
|
+
* 注意:`:memory:` 和 `file:xxx?mode=memory` 等内存数据库不会被缓存,
|
|
45
|
+
* 每次调用都会创建独立连接(SQLite 的 :memory: 为每个连接独立)。
|
|
46
|
+
*/
|
|
47
|
+
function acquireConnection(dbPath, walMode = true) {
|
|
48
|
+
if (dbPath === ":memory:" || dbPath.startsWith("file:") && dbPath.includes("mode=memory")) {
|
|
49
|
+
const db = new Database(dbPath);
|
|
50
|
+
if (walMode) db.run("PRAGMA journal_mode = WAL");
|
|
51
|
+
const localCache = /* @__PURE__ */ new Map();
|
|
52
|
+
let released = false;
|
|
53
|
+
return makeHandle(db, localCache, () => {
|
|
54
|
+
if (released) return;
|
|
55
|
+
released = true;
|
|
56
|
+
db.close();
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
const existing = pool.get(dbPath);
|
|
60
|
+
if (existing !== void 0) {
|
|
61
|
+
existing.refCount++;
|
|
62
|
+
let released = false;
|
|
63
|
+
return makeHandle(existing.db, existing.stmts, () => {
|
|
64
|
+
if (released) return;
|
|
65
|
+
released = true;
|
|
66
|
+
releaseConnection(dbPath);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
const db = new Database(dbPath);
|
|
70
|
+
if (walMode) db.run("PRAGMA journal_mode = WAL");
|
|
71
|
+
const stmts = /* @__PURE__ */ new Map();
|
|
72
|
+
pool.set(dbPath, {
|
|
73
|
+
db,
|
|
74
|
+
refCount: 1,
|
|
75
|
+
stmts
|
|
76
|
+
});
|
|
77
|
+
let released = false;
|
|
78
|
+
return makeHandle(db, stmts, () => {
|
|
79
|
+
if (released) return;
|
|
80
|
+
released = true;
|
|
81
|
+
releaseConnection(dbPath);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
/** 内部释放逻辑 */
|
|
85
|
+
function releaseConnection(dbPath) {
|
|
86
|
+
const entry = pool.get(dbPath);
|
|
87
|
+
if (entry === void 0) return;
|
|
88
|
+
entry.refCount--;
|
|
89
|
+
if (entry.refCount <= 0) {
|
|
90
|
+
pool.delete(dbPath);
|
|
91
|
+
entry.db.close();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
//#endregion
|
|
95
|
+
export { acquireConnection };
|
|
@@ -19,7 +19,8 @@ interface SqliteRepositoryBackend extends RepositoryBackend {
|
|
|
19
19
|
/**
|
|
20
20
|
* 创建基于 SQLite 文件的完整仓库后端
|
|
21
21
|
*
|
|
22
|
-
*
|
|
22
|
+
* 内部通过连接池获取 Database 实例并组合三个子 store。
|
|
23
|
+
* 相同 dbPath 复用同一连接,引用计数归零时自动关闭。
|
|
23
24
|
* 返回的 backend 实现了 [Symbol.dispose](),可使用 `using` 语法
|
|
24
25
|
* 或在不再使用时手动调用 `backend[Symbol.dispose]()`。
|
|
25
26
|
*
|
package/dist/backend/sqlite.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { HEADS_PREFIX, HEAD_REF } from "../core/types/refs.mjs";
|
|
|
2
2
|
import { createSqliteObjectStore } from "../odb/sqlite.mjs";
|
|
3
3
|
import { createSqliteRefStore } from "../refs/sqlite.mjs";
|
|
4
4
|
import { createSqliteShallowStore } from "../refs/shallow/sqlite.mjs";
|
|
5
|
-
import {
|
|
5
|
+
import { acquireConnection } from "./sqlite-pool.mjs";
|
|
6
6
|
//#region src/backend/sqlite.ts
|
|
7
7
|
/**
|
|
8
8
|
* 基于 SQLite 的仓库后端
|
|
@@ -12,11 +12,15 @@ import { Database } from "bun:sqlite";
|
|
|
12
12
|
*
|
|
13
13
|
* 支持 [Symbol.dispose]() 释放数据库连接,
|
|
14
14
|
* 可使用 `using` 语法管理生命周期。
|
|
15
|
+
*
|
|
16
|
+
* 数据库连接通过全局连接池管理(引用计数),
|
|
17
|
+
* 相同 dbPath 复用同一连接,避免反复打开同一个数据库文件。
|
|
15
18
|
*/
|
|
16
19
|
/**
|
|
17
20
|
* 创建基于 SQLite 文件的完整仓库后端
|
|
18
21
|
*
|
|
19
|
-
*
|
|
22
|
+
* 内部通过连接池获取 Database 实例并组合三个子 store。
|
|
23
|
+
* 相同 dbPath 复用同一连接,引用计数归零时自动关闭。
|
|
20
24
|
* 返回的 backend 实现了 [Symbol.dispose](),可使用 `using` 语法
|
|
21
25
|
* 或在不再使用时手动调用 `backend[Symbol.dispose]()`。
|
|
22
26
|
*
|
|
@@ -37,21 +41,20 @@ import { Database } from "bun:sqlite";
|
|
|
37
41
|
* ```
|
|
38
42
|
*/
|
|
39
43
|
function createSqliteRepositoryBackend(dbPath, options = {}) {
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
db.run("CREATE TABLE IF NOT EXISTS
|
|
43
|
-
db.run("CREATE TABLE IF NOT EXISTS
|
|
44
|
-
db.run("
|
|
45
|
-
db.run("INSERT OR IGNORE INTO refs (name, target) VALUES (?, ?)", [HEAD_REF, `ref: ${HEADS_PREFIX}main`]);
|
|
44
|
+
const conn = acquireConnection(dbPath, options.walMode !== false);
|
|
45
|
+
conn.db.run("CREATE TABLE IF NOT EXISTS objects (hash TEXT PRIMARY KEY, type TEXT NOT NULL, content BLOB NOT NULL)");
|
|
46
|
+
conn.db.run("CREATE TABLE IF NOT EXISTS refs (name TEXT PRIMARY KEY, target TEXT NOT NULL)");
|
|
47
|
+
conn.db.run("CREATE TABLE IF NOT EXISTS shallow (hash TEXT PRIMARY KEY)");
|
|
48
|
+
conn.db.run("INSERT OR IGNORE INTO refs (name, target) VALUES (?, ?)", [HEAD_REF, `ref: ${HEADS_PREFIX}main`]);
|
|
46
49
|
return {
|
|
47
50
|
gitDir: dbPath,
|
|
48
|
-
objects: createSqliteObjectStore(
|
|
49
|
-
refs: createSqliteRefStore(
|
|
50
|
-
shallow: createSqliteShallowStore(
|
|
51
|
+
objects: createSqliteObjectStore(conn),
|
|
52
|
+
refs: createSqliteRefStore(conn),
|
|
53
|
+
shallow: createSqliteShallowStore(conn),
|
|
51
54
|
packs: null,
|
|
52
|
-
/** 释放 SQLite
|
|
55
|
+
/** 释放 SQLite 数据库连接(引用计数减一) */
|
|
53
56
|
[Symbol.dispose]() {
|
|
54
|
-
|
|
57
|
+
conn.release();
|
|
55
58
|
}
|
|
56
59
|
};
|
|
57
60
|
}
|
package/dist/odb/sqlite.d.mts
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
import { ObjectDatabase } from "../core/types/odb.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { SqliteConnectionHandle } from "../backend/sqlite-pool.mjs";
|
|
3
3
|
|
|
4
4
|
//#region src/odb/sqlite.d.ts
|
|
5
5
|
/**
|
|
6
6
|
* 创建基于 SQLite 的对象数据库
|
|
7
7
|
*
|
|
8
|
-
* @param
|
|
8
|
+
* @param conn - SQLite 连接池句柄(含 statement 缓存)
|
|
9
9
|
* @returns 符合 ObjectDatabase 接口的存储后端
|
|
10
10
|
*
|
|
11
11
|
* @example
|
|
12
12
|
* ```ts
|
|
13
|
-
* import {
|
|
14
|
-
*
|
|
15
|
-
* const store = createSqliteObjectStore(
|
|
13
|
+
* import { acquireConnection } from "nano-git/backend/sqlite";
|
|
14
|
+
* using conn = acquireConnection("/tmp/repo.sqlite");
|
|
15
|
+
* const store = createSqliteObjectStore(conn);
|
|
16
16
|
*
|
|
17
17
|
* store.ingest(raw);
|
|
18
18
|
* const obj = store.read(hash);
|
|
19
19
|
* ```
|
|
20
20
|
*/
|
|
21
|
-
declare function createSqliteObjectStore(
|
|
21
|
+
declare function createSqliteObjectStore(conn: SqliteConnectionHandle): ObjectDatabase;
|
|
22
22
|
//#endregion
|
|
23
23
|
export { createSqliteObjectStore };
|
package/dist/odb/sqlite.mjs
CHANGED
|
@@ -14,27 +14,27 @@ import { hashObject } from "../core/hash-digest.mjs";
|
|
|
14
14
|
/**
|
|
15
15
|
* 创建基于 SQLite 的对象数据库
|
|
16
16
|
*
|
|
17
|
-
* @param
|
|
17
|
+
* @param conn - SQLite 连接池句柄(含 statement 缓存)
|
|
18
18
|
* @returns 符合 ObjectDatabase 接口的存储后端
|
|
19
19
|
*
|
|
20
20
|
* @example
|
|
21
21
|
* ```ts
|
|
22
|
-
* import {
|
|
23
|
-
*
|
|
24
|
-
* const store = createSqliteObjectStore(
|
|
22
|
+
* import { acquireConnection } from "nano-git/backend/sqlite";
|
|
23
|
+
* using conn = acquireConnection("/tmp/repo.sqlite");
|
|
24
|
+
* const store = createSqliteObjectStore(conn);
|
|
25
25
|
*
|
|
26
26
|
* store.ingest(raw);
|
|
27
27
|
* const obj = store.read(hash);
|
|
28
28
|
* ```
|
|
29
29
|
*/
|
|
30
|
-
function createSqliteObjectStore(
|
|
31
|
-
const selectStmt =
|
|
32
|
-
const existsStmt =
|
|
33
|
-
const insertStmt =
|
|
34
|
-
const deleteStmt =
|
|
35
|
-
const listStmt =
|
|
30
|
+
function createSqliteObjectStore(conn) {
|
|
31
|
+
const selectStmt = conn.prepare("SELECT hash, type, content FROM objects WHERE hash = ?");
|
|
32
|
+
const existsStmt = conn.prepare("SELECT 1 FROM objects WHERE hash = ?");
|
|
33
|
+
const insertStmt = conn.prepare("INSERT OR IGNORE INTO objects (hash, type, content) VALUES (?, ?, ?)");
|
|
34
|
+
const deleteStmt = conn.prepare("DELETE FROM objects WHERE hash = ?");
|
|
35
|
+
const listStmt = conn.prepare("SELECT hash FROM objects ORDER BY hash");
|
|
36
36
|
/** 批量插入的事务包装 */
|
|
37
|
-
const ingestManyTx = db.transaction((objects) => {
|
|
37
|
+
const ingestManyTx = conn.db.transaction((objects) => {
|
|
38
38
|
for (const raw of objects) {
|
|
39
39
|
const expectedHash = hashObject(raw.type, raw.content);
|
|
40
40
|
if (expectedHash !== raw.hash) throw new Error(`RawGitObject hash mismatch: expected ${expectedHash}, got ${raw.hash}`);
|
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
import { ShallowStore } from "../../core/types/shallow.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { SqliteConnectionHandle } from "../../backend/sqlite-pool.mjs";
|
|
3
3
|
|
|
4
4
|
//#region src/refs/shallow/sqlite.d.ts
|
|
5
5
|
/**
|
|
6
6
|
* 创建基于 SQLite 的 shallow 边界存储
|
|
7
7
|
*
|
|
8
|
-
* @param
|
|
8
|
+
* @param conn - SQLite 连接池句柄(含 statement 缓存)
|
|
9
9
|
* @returns 符合 ShallowStore 接口的存储后端
|
|
10
10
|
*
|
|
11
11
|
* @example
|
|
12
12
|
* ```ts
|
|
13
|
-
* import {
|
|
14
|
-
*
|
|
15
|
-
* const store = createSqliteShallowStore(
|
|
13
|
+
* import { acquireConnection } from "nano-git/backend/sqlite";
|
|
14
|
+
* using conn = acquireConnection("/tmp/repo.sqlite");
|
|
15
|
+
* const store = createSqliteShallowStore(conn);
|
|
16
16
|
*
|
|
17
17
|
* store.write([hashA, hashB]);
|
|
18
18
|
* console.log(store.isShallow(hashA)); // true
|
|
19
19
|
* ```
|
|
20
20
|
*/
|
|
21
|
-
declare function createSqliteShallowStore(
|
|
21
|
+
declare function createSqliteShallowStore(conn: SqliteConnectionHandle): ShallowStore;
|
|
22
22
|
//#endregion
|
|
23
23
|
export { createSqliteShallowStore };
|
|
@@ -13,32 +13,32 @@ import { sha1 } from "../../core/types.mjs";
|
|
|
13
13
|
/**
|
|
14
14
|
* 创建基于 SQLite 的 shallow 边界存储
|
|
15
15
|
*
|
|
16
|
-
* @param
|
|
16
|
+
* @param conn - SQLite 连接池句柄(含 statement 缓存)
|
|
17
17
|
* @returns 符合 ShallowStore 接口的存储后端
|
|
18
18
|
*
|
|
19
19
|
* @example
|
|
20
20
|
* ```ts
|
|
21
|
-
* import {
|
|
22
|
-
*
|
|
23
|
-
* const store = createSqliteShallowStore(
|
|
21
|
+
* import { acquireConnection } from "nano-git/backend/sqlite";
|
|
22
|
+
* using conn = acquireConnection("/tmp/repo.sqlite");
|
|
23
|
+
* const store = createSqliteShallowStore(conn);
|
|
24
24
|
*
|
|
25
25
|
* store.write([hashA, hashB]);
|
|
26
26
|
* console.log(store.isShallow(hashA)); // true
|
|
27
27
|
* ```
|
|
28
28
|
*/
|
|
29
|
-
function createSqliteShallowStore(
|
|
30
|
-
const selectAllStmt =
|
|
31
|
-
const selectExistsStmt =
|
|
32
|
-
const deleteAllStmt =
|
|
33
|
-
const insertStmt =
|
|
34
|
-
const deleteOneStmt =
|
|
29
|
+
function createSqliteShallowStore(conn) {
|
|
30
|
+
const selectAllStmt = conn.prepare("SELECT hash FROM shallow ORDER BY hash");
|
|
31
|
+
const selectExistsStmt = conn.prepare("SELECT 1 FROM shallow WHERE hash = ?");
|
|
32
|
+
const deleteAllStmt = conn.prepare("DELETE FROM shallow");
|
|
33
|
+
const insertStmt = conn.prepare("INSERT OR IGNORE INTO shallow (hash) VALUES (?)");
|
|
34
|
+
const deleteOneStmt = conn.prepare("DELETE FROM shallow WHERE hash = ?");
|
|
35
35
|
/** 全量替换事务 */
|
|
36
|
-
const replaceAllTx = db.transaction((boundaries) => {
|
|
36
|
+
const replaceAllTx = conn.db.transaction((boundaries) => {
|
|
37
37
|
deleteAllStmt.run();
|
|
38
38
|
for (const hash of boundaries) insertStmt.run(hash);
|
|
39
39
|
});
|
|
40
40
|
/** 增量更新事务 */
|
|
41
|
-
const applyUpdateTx = db.transaction((update) => {
|
|
41
|
+
const applyUpdateTx = conn.db.transaction((update) => {
|
|
42
42
|
for (const hash of update.unshallow) deleteOneStmt.run(hash);
|
|
43
43
|
for (const hash of update.shallow) insertStmt.run(hash);
|
|
44
44
|
});
|
package/dist/refs/sqlite.d.mts
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
import { RefStore } from "../core/types/refs.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { SqliteConnectionHandle } from "../backend/sqlite-pool.mjs";
|
|
3
3
|
|
|
4
4
|
//#region src/refs/sqlite.d.ts
|
|
5
5
|
/**
|
|
6
6
|
* 创建基于 SQLite 的 RefStore
|
|
7
7
|
*
|
|
8
|
-
* @param
|
|
8
|
+
* @param conn - SQLite 连接池句柄(含 statement 缓存)
|
|
9
9
|
* @returns 符合 RefStore 接口的存储后端(含事务支持)
|
|
10
10
|
*
|
|
11
11
|
* @example
|
|
12
12
|
* ```ts
|
|
13
|
-
* import {
|
|
14
|
-
*
|
|
15
|
-
* const store = createSqliteRefStore(
|
|
13
|
+
* import { acquireConnection } from "nano-git/backend/sqlite";
|
|
14
|
+
* using conn = acquireConnection("/tmp/repo.sqlite");
|
|
15
|
+
* const store = createSqliteRefStore(conn);
|
|
16
16
|
*
|
|
17
17
|
* store.write("refs/heads/main", "abc123");
|
|
18
18
|
* const content = store.read("refs/heads/main");
|
|
19
19
|
* ```
|
|
20
20
|
*/
|
|
21
|
-
declare function createSqliteRefStore(
|
|
21
|
+
declare function createSqliteRefStore(conn: SqliteConnectionHandle): RefStore;
|
|
22
22
|
//#endregion
|
|
23
23
|
export { createSqliteRefStore };
|
package/dist/refs/sqlite.mjs
CHANGED
|
@@ -13,26 +13,27 @@ import { validateRefName, validateRefPrefix } from "./names.mjs";
|
|
|
13
13
|
/**
|
|
14
14
|
* 创建基于 SQLite 的 RefStore
|
|
15
15
|
*
|
|
16
|
-
* @param
|
|
16
|
+
* @param conn - SQLite 连接池句柄(含 statement 缓存)
|
|
17
17
|
* @returns 符合 RefStore 接口的存储后端(含事务支持)
|
|
18
18
|
*
|
|
19
19
|
* @example
|
|
20
20
|
* ```ts
|
|
21
|
-
* import {
|
|
22
|
-
*
|
|
23
|
-
* const store = createSqliteRefStore(
|
|
21
|
+
* import { acquireConnection } from "nano-git/backend/sqlite";
|
|
22
|
+
* using conn = acquireConnection("/tmp/repo.sqlite");
|
|
23
|
+
* const store = createSqliteRefStore(conn);
|
|
24
24
|
*
|
|
25
25
|
* store.write("refs/heads/main", "abc123");
|
|
26
26
|
* const content = store.read("refs/heads/main");
|
|
27
27
|
* ```
|
|
28
28
|
*/
|
|
29
|
-
function createSqliteRefStore(
|
|
30
|
-
const selectStmt =
|
|
31
|
-
const selectExistsStmt =
|
|
32
|
-
const insertStmt =
|
|
33
|
-
const deleteStmt =
|
|
34
|
-
const listPrefixStmt =
|
|
35
|
-
const listAllStmt =
|
|
29
|
+
function createSqliteRefStore(conn) {
|
|
30
|
+
const selectStmt = conn.prepare("SELECT target FROM refs WHERE name = ?");
|
|
31
|
+
const selectExistsStmt = conn.prepare("SELECT 1 FROM refs WHERE name = ?");
|
|
32
|
+
const insertStmt = conn.prepare("INSERT OR REPLACE INTO refs (name, target) VALUES (?, ?)");
|
|
33
|
+
const deleteStmt = conn.prepare("DELETE FROM refs WHERE name = ?");
|
|
34
|
+
const listPrefixStmt = conn.prepare("SELECT name FROM refs WHERE name >= ? AND name < ? ORDER BY name");
|
|
35
|
+
const listAllStmt = conn.prepare("SELECT name FROM refs WHERE name LIKE 'refs/%' ORDER BY name");
|
|
36
|
+
const db = conn.db;
|
|
36
37
|
/**
|
|
37
38
|
* 开启一个新的事务
|
|
38
39
|
*
|
|
@@ -6,6 +6,18 @@ import { patchTree } from "../tree/tree-patch.mjs";
|
|
|
6
6
|
* 仓库对象操作组装
|
|
7
7
|
*/
|
|
8
8
|
/**
|
|
9
|
+
* 空 tree 的规范原始对象
|
|
10
|
+
*
|
|
11
|
+
* 空 tree 序列化后内容为空,header 为 "tree 0\0",
|
|
12
|
+
* 其 SHA-1 是 Git 通用已知常数。
|
|
13
|
+
* 预计算一份避免每次重复序列化和哈希。
|
|
14
|
+
*/
|
|
15
|
+
const EMPTY_TREE_RAW = {
|
|
16
|
+
hash: hashObject("tree", Buffer.alloc(0)),
|
|
17
|
+
type: "tree",
|
|
18
|
+
content: Buffer.alloc(0)
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
9
21
|
* 创建仓库对象相关操作
|
|
10
22
|
*
|
|
11
23
|
* @example
|
|
@@ -36,6 +48,10 @@ function createObjectRepositoryOperations(objects) {
|
|
|
36
48
|
return objects.list();
|
|
37
49
|
},
|
|
38
50
|
createTree(entries) {
|
|
51
|
+
if (entries.length === 0) {
|
|
52
|
+
objects.ingest(EMPTY_TREE_RAW);
|
|
53
|
+
return EMPTY_TREE_RAW.hash;
|
|
54
|
+
}
|
|
39
55
|
return writeObject(objects, {
|
|
40
56
|
type: "tree",
|
|
41
57
|
entries
|
package/dist/workdir/core.d.mts
CHANGED
|
@@ -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 };
|
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;
|
|
@@ -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);
|
package/dist/workdir/workdir.mjs
CHANGED
|
@@ -6,7 +6,6 @@ import { assertValidVirtualPath, normalizeDirectoryPath, parentPath, splitPathSe
|
|
|
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";
|
|
@@ -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,7 +220,6 @@ 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));
|
|
@@ -240,13 +228,6 @@ function openVirtualWorkdir(source, state) {
|
|
|
240
228
|
move(from, to) {
|
|
241
229
|
runInWriteTransaction(state, () => {
|
|
242
230
|
changeIndexPlanner.apply(changeIndexPlanner.planRewriteForRename(from, to));
|
|
243
|
-
const dirtyPaths = [from, to];
|
|
244
|
-
const toParent = parentPath(to);
|
|
245
|
-
if (toParent !== null) {
|
|
246
|
-
const segments = splitPathSegments(toParent);
|
|
247
|
-
for (let i = 0; i < segments.length; i++) dirtyPaths.push(segments.slice(0, i + 1).join("/"));
|
|
248
|
-
}
|
|
249
|
-
dirtyDirPlanner.rebuild(dirtyPaths);
|
|
250
231
|
}, invalidateDiffCaches, () => {
|
|
251
232
|
assertValidVirtualPath(from);
|
|
252
233
|
assertValidVirtualPath(to);
|
|
@@ -271,7 +252,6 @@ function openVirtualWorkdir(source, state) {
|
|
|
271
252
|
copy(from, to) {
|
|
272
253
|
runInWriteTransaction(state, () => {
|
|
273
254
|
changeIndexPlanner.apply(changeIndexPlanner.planWriteForCopy(from, to));
|
|
274
|
-
dirtyDirPlanner.rebuild([from, to]);
|
|
275
255
|
}, invalidateDiffCaches, () => {
|
|
276
256
|
assertValidVirtualPath(from);
|
|
277
257
|
assertValidVirtualPath(to);
|
|
@@ -286,7 +266,6 @@ function openVirtualWorkdir(source, state) {
|
|
|
286
266
|
revert(path) {
|
|
287
267
|
runInWriteTransaction(state, () => {
|
|
288
268
|
changeIndexPlanner.apply(changeIndexPlanner.planRefreshForPath(path));
|
|
289
|
-
dirtyDirPlanner.rebuild([path]);
|
|
290
269
|
}, invalidateDiffCaches, () => {
|
|
291
270
|
assertValidVirtualPath(path);
|
|
292
271
|
const resolved = resolvePath(source, state, path);
|
|
@@ -306,7 +285,6 @@ function openVirtualWorkdir(source, state) {
|
|
|
306
285
|
reset(baseTree) {
|
|
307
286
|
runInWriteTransaction(state, null, invalidateDiffCaches, () => {
|
|
308
287
|
state.reset(baseTree);
|
|
309
|
-
dirtyDirPlanner.clear();
|
|
310
288
|
});
|
|
311
289
|
}
|
|
312
290
|
};
|
|
@@ -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 };
|