nano-git 0.3.3 → 0.3.5

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.
@@ -14,7 +14,7 @@ interface NormalizedChangeRecord {
14
14
  readonly previous: VirtualDiffObject | null;
15
15
  /** 变更后对象 */
16
16
  readonly current: VirtualDiffObject | null;
17
- /** rename/copy 来源 */
17
+ /** move/copy 来源 */
18
18
  readonly source: VirtualDiffSource | null;
19
19
  }
20
20
  //#endregion
@@ -76,7 +76,7 @@ function rebuildNormalizedChangeIndex(source, state, cache) {
76
76
  previous: renameFrom.previous,
77
77
  current: addRecord.current,
78
78
  source: {
79
- kind: "rename",
79
+ kind: "move",
80
80
  path: renameFrom.path
81
81
  }
82
82
  });
@@ -155,17 +155,17 @@ function refreshChangeRecordForPath(source, state, path, cache) {
155
155
  state.setChangeRecord(nextRecord);
156
156
  }
157
157
  /**
158
- * 将单一路径的变更记录折叠为 rename 目标路径。
158
+ * 将单一路径的变更记录折叠为 move 目标路径。
159
159
  *
160
- * 仅适用于叶子节点 rename
160
+ * 仅适用于叶子节点 move
161
161
  * 目录及无法判定来源的情况应由调用方回退到全量重建。
162
162
  */
163
163
  function rewriteChangeRecordForRename(source, state, from, to, cache) {
164
164
  const previousRecord = state.getChangeRecord(from);
165
165
  const currentTarget = snapshotCurrentEntryAtPath(source, state, to, cache);
166
- if (currentTarget === null) throw new Error(`Cannot rewrite rename change record for missing path: ${to}`);
166
+ if (currentTarget === null) throw new Error(`Cannot rewrite move change record for missing path: ${to}`);
167
167
  const nextRecord = computeRenameRecordForPath(source, state, from, to, previousRecord, currentTarget);
168
- if (nextRecord === null) throw new Error(`Cannot rewrite rename change record from '${from}' to '${to}'`);
168
+ if (nextRecord === null) throw new Error(`Cannot rewrite move change record from '${from}' to '${to}'`);
169
169
  state.deleteChangeRecord(from);
170
170
  state.setChangeRecord(nextRecord);
171
171
  }
@@ -279,7 +279,7 @@ function computeRenameRecordForPath(source, state, from, to, previousRecord, cur
279
279
  if (derivedFromPrevious !== void 0) return derivedFromPrevious;
280
280
  const baseEntry = baseSnapshotEntryAtPath(source, state.readBaseTree(), from);
281
281
  if (baseEntry === null) return null;
282
- return createNormalizedChangeRecord(to, baseEntry.object, currentTarget.object, createDiffSource("rename", from));
282
+ return createNormalizedChangeRecord(to, baseEntry.object, currentTarget.object, createDiffSource("move", from));
283
283
  }
284
284
  function deriveCopySource(from, sourceRecord, source, state) {
285
285
  const fromRecordSource = sourceRecord?.source;
@@ -288,7 +288,7 @@ function deriveCopySource(from, sourceRecord, source, state) {
288
288
  return null;
289
289
  }
290
290
  function preserveLineageRecordForPath(path, previousRecord, currentEntry) {
291
- if (previousRecord?.source?.kind === "rename" && previousRecord.previous !== null) {
291
+ if (previousRecord?.source?.kind === "move" && previousRecord.previous !== null) {
292
292
  if (currentEntry === null) return null;
293
293
  return createNormalizedChangeRecord(path, previousRecord.previous, currentEntry.object, previousRecord.source);
294
294
  }
@@ -302,7 +302,7 @@ function deriveRenameRecordFromPreviousRecord(from, to, previousRecord, currentT
302
302
  if (previousRecord.current === null) return null;
303
303
  if (previousRecord.source !== null) return createNormalizedChangeRecord(to, previousRecord.previous, currentTarget.object, previousRecord.source);
304
304
  if (previousRecord.previous === null) return createNormalizedChangeRecord(to, null, currentTarget.object);
305
- return createNormalizedChangeRecord(to, previousRecord.previous, currentTarget.object, createDiffSource("rename", from));
305
+ return createNormalizedChangeRecord(to, previousRecord.previous, currentTarget.object, createDiffSource("move", from));
306
306
  }
307
307
  function snapshotCurrentEntryAtPath(source, state, path, cache) {
308
308
  const resolved = resolveCurrentLeafAtPath(source, state, path);
@@ -50,11 +50,11 @@ interface VirtualDiffObject {
50
50
  readonly hash: SHA1;
51
51
  }
52
52
  /**
53
- * rename/copy 来源描述
53
+ * move/copy 来源描述
54
54
  */
55
55
  interface VirtualDiffSource {
56
56
  /** 来源类型 */
57
- readonly kind: "rename" | "copy";
57
+ readonly kind: "move" | "copy";
58
58
  /** 来源路径 */
59
59
  readonly path: string;
60
60
  }
@@ -77,7 +77,7 @@ interface VirtualDiffChanges {
77
77
  type VirtualDiffEntry = {
78
78
  /** 新建路径 */readonly kind: "create"; /** 当前路径 */
79
79
  readonly path: string; /** 当前对象 */
80
- readonly current: VirtualDiffObject; /** rename/copy 的来源 */
80
+ readonly current: VirtualDiffObject; /** move/copy 的来源 */
81
81
  readonly source?: VirtualDiffSource;
82
82
  } | {
83
83
  /** 删除路径 */readonly kind: "remove"; /** 当前路径 */
@@ -88,7 +88,7 @@ type VirtualDiffEntry = {
88
88
  readonly path: string; /** 更新前对象 */
89
89
  readonly previous: VirtualDiffObject; /** 更新后对象 */
90
90
  readonly current: VirtualDiffObject; /** 变化维度 */
91
- readonly changes: VirtualDiffChanges; /** rename/copy 的来源 */
91
+ readonly changes: VirtualDiffChanges; /** move/copy 的来源 */
92
92
  readonly source?: VirtualDiffSource;
93
93
  };
94
94
  /**
@@ -140,17 +140,31 @@ interface VirtualWorkdir {
140
140
  }): void;
141
141
  /** 写入符号链接(新建或覆盖) */
142
142
  writeLink(path: string, target: string): void;
143
- /** 创建目录(含必要父目录) */
144
- mkdir(path: string): void;
145
- /** 删除路径(文件、目录或符号链接) */
146
- delete(path: string): void;
147
143
  /**
148
- * 重命名路径
144
+ * 创建目录
145
+ *
146
+ * 默认要求各级父目录已存在;`recursive: true` 时自动创建缺失的父目录,
147
+ * 且目标路径已是目录时不报错。路径上任意段为文件(非目录)时抛出 `VirtualNotDirectoryError`。
148
+ */
149
+ mkdir(path: string, options?: {
150
+ readonly recursive?: boolean;
151
+ }): void;
152
+ /**
153
+ * 删除路径(文件、目录或符号链接)
154
+ *
155
+ * 默认要求路径已存在;`force: true` 时路径不存在则静默忽略(与 Node `fs.rm` 的 `force` 语义一致)。
156
+ */
157
+ delete(path: string, options?: {
158
+ readonly force?: boolean;
159
+ }): void;
160
+ /**
161
+ * 移动路径(可跨目录树)
149
162
  *
150
163
  * 只做路径重绑定,不退化为 delete + write。
151
- * 目录重命名后,子项保持懒加载。
164
+ * 目标父目录不存在时会自动创建中间目录。
165
+ * 目录移动后,子项保持懒加载。
152
166
  */
153
- rename(from: string, to: string): void;
167
+ move(from: string, to: string): void;
154
168
  /**
155
169
  * 复制路径
156
170
  *
@@ -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 {
@@ -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);
@@ -381,8 +381,10 @@ function readDiffObjectMode(raw) {
381
381
  if (raw === "100644" || raw === "100755" || raw === "120000") return raw;
382
382
  throw new Error(`Invalid SQLite workdir diff object mode: ${raw}`);
383
383
  }
384
+ /** 解析 diff 来源种类;旧版 `rename` 读入时规范为 `move`。 */
384
385
  function readDiffSourceKind(raw) {
385
- if (raw === "rename" || raw === "copy") return raw;
386
+ if (raw === "move" || raw === "copy") return raw;
387
+ if (raw === "rename") return "move";
386
388
  throw new Error(`Invalid SQLite workdir diff source kind: ${raw}`);
387
389
  }
388
390
  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,7 +2,7 @@ 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 } 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";
@@ -15,7 +15,7 @@ import { writeTreeFromSession } from "./write-tree.mjs";
15
15
  /**
16
16
  * VirtualWorkdir 行为编排
17
17
  *
18
- * Phase 5:完整文件/目录 rename 与 copy 语义。
18
+ * Phase 5:完整文件/目录 move 与 copy 语义。
19
19
  */
20
20
  /**
21
21
  * 基于 ObjectDatabase 创建 VirtualWorkdir
@@ -90,6 +90,35 @@ function openVirtualWorkdir(source, state) {
90
90
  });
91
91
  const dirtyDirPlanner = createDirtyDirPlanner(source, state);
92
92
  refreshChangeIndex();
93
+ const createDirectoryAtPath = (path) => {
94
+ const target = requireMissingWriteTarget(source, state, path);
95
+ const nodeId = createNodeId();
96
+ const newNode = {
97
+ id: nodeId,
98
+ origin: { kind: "none" },
99
+ state: {
100
+ kind: "directory",
101
+ overlay: {
102
+ addedEntries: /* @__PURE__ */ new Map(),
103
+ deletedNames: /* @__PURE__ */ new Set()
104
+ }
105
+ }
106
+ };
107
+ state.setNode(newNode);
108
+ updateParentOverlay(state, target.parentNode.id, overlayBindEntry(target.parentNode.state.overlay, target.name, nodeId));
109
+ };
110
+ const mkdirRecursive = (path) => {
111
+ const segments = splitPathSegments(path);
112
+ for (let i = 0; i < segments.length; i++) {
113
+ const partialPath = segments.slice(0, i + 1).join("/");
114
+ const resolved = resolvePath(source, state, partialPath);
115
+ if (resolved.found && resolved.node !== null) {
116
+ if (resolved.node.state.kind !== "directory") throw new VirtualNotDirectoryError(partialPath);
117
+ continue;
118
+ }
119
+ createDirectoryAtPath(partialPath);
120
+ }
121
+ };
93
122
  return {
94
123
  get baseTree() {
95
124
  return state.readBaseTree();
@@ -141,23 +170,18 @@ function openVirtualWorkdir(source, state) {
141
170
  if (node.origin.kind === "repo-blob") return readRepoBlobContent(source, node.origin.hash, path).toString("utf-8");
142
171
  throw new VirtualPathNotFoundError(path);
143
172
  },
144
- mkdir(path) {
145
- runInWriteTransaction(state, () => dirtyDirPlanner.rebuild([path]), invalidateDiffCaches, () => {
146
- const target = requireMissingWriteTarget(source, state, path);
147
- const nodeId = createNodeId();
148
- const newNode = {
149
- id: nodeId,
150
- origin: { kind: "none" },
151
- state: {
152
- kind: "directory",
153
- overlay: {
154
- addedEntries: /* @__PURE__ */ new Map(),
155
- deletedNames: /* @__PURE__ */ new Set()
156
- }
157
- }
158
- };
159
- state.setNode(newNode);
160
- updateParentOverlay(state, target.parentNode.id, overlayBindEntry(target.parentNode.state.overlay, target.name, nodeId));
173
+ mkdir(path, options) {
174
+ 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, () => {
183
+ if (recursive) mkdirRecursive(path);
184
+ else createDirectoryAtPath(path);
161
185
  });
162
186
  },
163
187
  writeFile(path, content, options) {
@@ -201,7 +225,10 @@ function openVirtualWorkdir(source, state) {
201
225
  if (writeTarget.existing === null || writeTarget.parentNode.state.overlay.addedEntries.has(writeTarget.name)) updateParentOverlay(state, writeTarget.parentNode.id, overlayBindEntry(writeTarget.parentNode.state.overlay, writeTarget.name, nodeId));
202
226
  });
203
227
  },
204
- delete(path) {
228
+ delete(path, options) {
229
+ if (options?.force === true) {
230
+ if (path !== "" && !resolvePath(source, state, path).found) return;
231
+ }
205
232
  runInWriteTransaction(state, () => {
206
233
  changeIndexPlanner.apply(changeIndexPlanner.planRefreshForPath(path, { treatMissingAsIncremental: true }));
207
234
  dirtyDirPlanner.rebuild([path]);
@@ -210,21 +237,29 @@ function openVirtualWorkdir(source, state) {
210
237
  updateParentOverlay(state, target.parentNode.id, overlayTombstoneEntry(target.parentNode.state.overlay, target.name));
211
238
  });
212
239
  },
213
- rename(from, to) {
240
+ move(from, to) {
214
241
  runInWriteTransaction(state, () => {
215
242
  changeIndexPlanner.apply(changeIndexPlanner.planRewriteForRename(from, to));
216
- dirtyDirPlanner.rebuild([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);
217
250
  }, invalidateDiffCaches, () => {
218
251
  assertValidVirtualPath(from);
219
252
  assertValidVirtualPath(to);
220
253
  if (from === to) return;
254
+ const toParent = parentPath(to);
255
+ if (toParent !== null) mkdirRecursive(toParent);
221
256
  const { from: fromTarget, to: toTarget } = resolveWriteTransfer(source, state, from, to);
222
257
  const sourceNode = fromTarget.existing.node;
223
258
  if (toTarget.existing !== null) throw new VirtualPathAlreadyExistsError(to);
224
259
  if (sourceNode.state.kind === "directory") {
225
260
  const toPath = to;
226
261
  const fromPath = from;
227
- if (toPath.startsWith(fromPath + "/") || toPath === fromPath) throw new Error(`Cannot rename '${from}' to '${to}': destination is a subdirectory of source`);
262
+ if (toPath.startsWith(fromPath + "/") || toPath === fromPath) throw new Error(`Cannot move '${from}' to '${to}': destination is a subdirectory of source`);
228
263
  }
229
264
  if (fromTarget.parentNode.id === toTarget.parentNode.id) updateParentOverlay(state, fromTarget.parentNode.id, overlayRenameEntry(fromTarget.parentNode.state.overlay, fromTarget.name, toTarget.name, sourceNode.id));
230
265
  else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nano-git",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "files": [
5
5
  "dist"
6
6
  ],