nano-git 0.3.2 → 0.3.3

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/README.md CHANGED
@@ -381,7 +381,7 @@ Git 使用内容寻址文件系统,所有对象通过 SHA-1 哈希寻址:
381
381
 
382
382
  存储目录内容,每个条目包含:
383
383
 
384
- - 文件模式(如 `100644` 普通文件,`100755` 可执行文件,`040000` 目录)
384
+ - 文件模式(如 `100644` 普通文件,`100755` 可执行文件,`040000` 目录,API 中均使用规范 6 位形式)
385
385
  - 文件名
386
386
  - 指向 blob 或子 tree 的哈希
387
387
 
@@ -48,7 +48,7 @@ interface GitBlob {
48
48
  }
49
49
  /** Tree 条目 — 目录中的一个文件或子目录 */
50
50
  interface TreeEntry {
51
- /** 文件模式(如 "100644" 普通文件, "100755" 可执行文件, "40000" 目录) */
51
+ /** 文件模式(如 "100644" 普通文件, "100755" 可执行文件, "040000" 目录) */
52
52
  mode: string;
53
53
  /** 文件/目录名 */
54
54
  name: string;
@@ -4,6 +4,8 @@ import { GitTree } from "../core/types.mjs";
4
4
  /**
5
5
  * 序列化 Tree 对象
6
6
  *
7
+ * 写入时自动将规范 mode 转换为磁盘格式(如 "040000" → "40000")。
8
+ *
7
9
  * @example
8
10
  * ```ts
9
11
  * const tree: GitTree = {
@@ -16,6 +18,8 @@ import { GitTree } from "../core/types.mjs";
16
18
  declare function serializeTree(tree: GitTree): Buffer;
17
19
  /**
18
20
  * 反序列化 Tree 对象
21
+ *
22
+ * 读取时自动将磁盘 mode 转换为规范形式(如 "40000" → "040000")。
19
23
  */
20
24
  declare function deserializeTree(content: Buffer): GitTree;
21
25
  //#endregion
@@ -9,10 +9,58 @@ import { sha1 } from "../core/types.mjs";
9
9
  * - mode: 文件模式(如 "100644")
10
10
  * - name: 文件名
11
11
  * - hash: 20 字节的原始 SHA-1(不是十六进制字符串)
12
+ *
13
+ * mode 规范化说明:
14
+ * Git CLI 显示目录 mode 为 "040000"(6 位八进制,前导补零),但磁盘上存储为 "40000"(5 字节)。
15
+ * 本模块在序列化/反序列化边界做双向转换,内部统一使用规范形式("040000")。
16
+ */
17
+ /**
18
+ * 目录 mode 的磁盘格式(无前导零,5 字节)
12
19
  */
20
+ const DIR_MODE_ON_DISK = "40000";
21
+ /**
22
+ * 目录 mode 的规范格式(有前导零,6 字节,与 git cat-file -p 显示一致)
23
+ */
24
+ const DIR_MODE_CANONICAL = "040000";
25
+ /**
26
+ * 将 mode 转换为规范形式(6 位八进制)
27
+ *
28
+ * 读取磁盘 tree 条目时调用:将 "40000" → "040000",其他 mode 不变。
29
+ *
30
+ * @param mode - 来自磁盘的 mode 字符串
31
+ * @returns 规范化的 mode 字符串
32
+ *
33
+ * @example
34
+ * ```ts
35
+ * toCanonicalMode("40000") // => "040000"
36
+ * toCanonicalMode("100644") // => "100644"
37
+ * ```
38
+ */
39
+ function toCanonicalMode(mode) {
40
+ return mode === DIR_MODE_ON_DISK ? DIR_MODE_CANONICAL : mode;
41
+ }
42
+ /**
43
+ * 将 mode 转换为磁盘格式(无多余前导零)
44
+ *
45
+ * 写入磁盘 tree 条目时调用:将 "040000" → "40000",其他 mode 不变。
46
+ *
47
+ * @param mode - 规范形式的 mode 字符串
48
+ * @returns 磁盘形式的 mode 字符串
49
+ *
50
+ * @example
51
+ * ```ts
52
+ * toOnDiskMode("040000") // => "40000"
53
+ * toOnDiskMode("100644") // => "100644"
54
+ * ```
55
+ */
56
+ function toOnDiskMode(mode) {
57
+ return mode === DIR_MODE_CANONICAL ? DIR_MODE_ON_DISK : mode;
58
+ }
13
59
  /**
14
60
  * 序列化 Tree 对象
15
61
  *
62
+ * 写入时自动将规范 mode 转换为磁盘格式(如 "040000" → "40000")。
63
+ *
16
64
  * @example
17
65
  * ```ts
18
66
  * const tree: GitTree = {
@@ -25,7 +73,8 @@ import { sha1 } from "../core/types.mjs";
25
73
  function serializeTree(tree) {
26
74
  const buffers = [];
27
75
  for (const entry of tree.entries) {
28
- const entryHeader = Buffer.from(`${entry.mode} ${entry.name}\0`, "utf-8");
76
+ const mode = toOnDiskMode(entry.mode);
77
+ const entryHeader = Buffer.from(`${mode} ${entry.name}\0`, "utf-8");
29
78
  const entryHash = Buffer.from(entry.hash, "hex");
30
79
  if (entryHash.length !== 20) throw new InvalidObjectError(`invalid SHA-1 hash length: ${entryHash.length}`);
31
80
  buffers.push(entryHeader, entryHash);
@@ -34,6 +83,8 @@ function serializeTree(tree) {
34
83
  }
35
84
  /**
36
85
  * 反序列化 Tree 对象
86
+ *
87
+ * 读取时自动将磁盘 mode 转换为规范形式(如 "40000" → "040000")。
37
88
  */
38
89
  function deserializeTree(content) {
39
90
  const entries = [];
@@ -51,7 +102,7 @@ function deserializeTree(content) {
51
102
  if (hashEnd > content.length) throw new InvalidObjectError("tree: truncated hash");
52
103
  const hash = content.subarray(hashStart, hashEnd).toString("hex");
53
104
  entries.push({
54
- mode,
105
+ mode: toCanonicalMode(mode),
55
106
  name,
56
107
  hash: sha1(hash)
57
108
  });
@@ -98,7 +98,7 @@ function processOpsSequentially(objects, rootHash, ops) {
98
98
  * 在 tree 中查找路径对应的条目
99
99
  *
100
100
  * 沿路径依次读取 tree 对象,最后一段返回目标条目。
101
- * 中间段必须为目录(mode "40000"),否则抛出异常。
101
+ * 中间段必须为目录(mode "040000"),否则抛出异常。
102
102
  */
103
103
  function findEntryByPath(objects, treeHash, path) {
104
104
  const segments = path.split("/");
@@ -109,7 +109,7 @@ function findEntryByPath(objects, treeHash, path) {
109
109
  const entry = obj.entries.find((e) => e.name === segments[i]);
110
110
  if (!entry) return null;
111
111
  if (i === segments.length - 1) return entry;
112
- if (entry.mode !== "40000") throw new Error(`Cannot access '${segments.slice(0, i + 1).join("/")}': not a directory (mode: ${entry.mode})`);
112
+ if (entry.mode !== "040000") throw new Error(`Cannot access '${segments.slice(0, i + 1).join("/")}': not a directory (mode: ${entry.mode})`);
113
113
  currentHash = entry.hash;
114
114
  }
115
115
  return null;
@@ -174,7 +174,7 @@ function applyPatchRecursive(objects, treeHash, ops, prefix, upsertedPaths) {
174
174
  for (const [name, childOps] of deeperOps) {
175
175
  const existingEntry = existingEntries.find((e) => e.name === name);
176
176
  const existingHash = existingEntry?.hash ?? null;
177
- if (existingEntry !== void 0 && existingEntry.mode !== "40000") throw new Error(`Cannot access '${prefix}${name}': existing entry is not a directory (mode: ${existingEntry.mode})`);
177
+ if (existingEntry !== void 0 && existingEntry.mode !== "040000") throw new Error(`Cannot access '${prefix}${name}': existing entry is not a directory (mode: ${existingEntry.mode})`);
178
178
  if (existingHash === null) {
179
179
  const hasUpsertInBatch = childOps.some((op) => op.op === "upsert") || childOps.some((op) => op.op === "delete" && upsertedPaths.has(`${prefix}${name}/${op.path}`));
180
180
  if (childOps.every((op) => op.op === "delete") && !hasUpsertInBatch) {
@@ -185,7 +185,7 @@ function applyPatchRecursive(objects, treeHash, ops, prefix, upsertedPaths) {
185
185
  const result = applyPatchRecursive(objects, existingHash, childOps, `${prefix}${name}/`, upsertedPaths);
186
186
  writtenTrees.push(...result.written);
187
187
  finalEntryMap.set(name, {
188
- mode: "40000",
188
+ mode: "040000",
189
189
  name,
190
190
  hash: result.hash
191
191
  });
@@ -58,7 +58,7 @@ function walkTreeRecursive(objects, treeHash, prefix, result, visit) {
58
58
  };
59
59
  if (result) result.push(entryWithPath);
60
60
  if (visit) visit(entryWithPath);
61
- if (entry.mode === "40000") walkTreeRecursive(objects, entry.hash, path, result, visit);
61
+ if (entry.mode === "040000") walkTreeRecursive(objects, entry.hash, path, result, visit);
62
62
  }
63
63
  }
64
64
  //#endregion
@@ -31,7 +31,7 @@ function writeTreeRecursive(store, dirPath) {
31
31
  if (stat.isDirectory()) {
32
32
  const subtreeHash = writeTreeRecursive(store, fullPath);
33
33
  entries.push({
34
- mode: "40000",
34
+ mode: "040000",
35
35
  name,
36
36
  hash: subtreeHash
37
37
  });
@@ -195,7 +195,7 @@ function snapshotBaseTree(source, treeHash, dirPath) {
195
195
  const out = [];
196
196
  for (const entry of tree.entries) {
197
197
  const path = joinChildPath(dirPath, entry.name);
198
- if (entry.mode === "40000") {
198
+ if (entry.mode === "040000") {
199
199
  out.push(...snapshotBaseTree(source, entry.hash, path));
200
200
  continue;
201
201
  }
@@ -18,7 +18,7 @@ type VirtualEntryKind = "blob" | "tree" | "symlink";
18
18
  interface VirtualEntryStat {
19
19
  /** 条目种类 */
20
20
  readonly kind: VirtualEntryKind;
21
- /** Git 文件模式(如 "100644"、"100755"、"40000"、"120000") */
21
+ /** Git 文件模式(如 "100644"、"100755"、"040000"、"120000") */
22
22
  readonly mode: string;
23
23
  /** 文件大小(对 blob 和 symlink 有效;目录返回 0) */
24
24
  readonly size: number;
@@ -28,7 +28,7 @@ function readRepoBlobContent(source, hash, path) {
28
28
  * 根据 tree 条目构造节点 origin
29
29
  */
30
30
  function treeEntryToNodeOrigin(entry) {
31
- if (entry.mode === "40000") return {
31
+ if (entry.mode === "040000") return {
32
32
  kind: "repo-tree",
33
33
  hash: entry.hash
34
34
  };
@@ -47,7 +47,7 @@ function treeEntryToNodeOrigin(entry) {
47
47
  * Git mode 转为 VirtualEntryKind
48
48
  */
49
49
  function modeToVirtualEntryKind(mode) {
50
- if (mode === "40000") return "tree";
50
+ if (mode === "040000") return "tree";
51
51
  if (mode === "120000") return "symlink";
52
52
  return "blob";
53
53
  }
@@ -272,7 +272,7 @@ function ensureNodeFromTreeEntry(state, entry) {
272
272
  if (state.getNode(id) === null) {
273
273
  const origin = treeEntryToNodeOrigin(entry);
274
274
  let nodeState;
275
- if (entry.mode === "40000") nodeState = {
275
+ if (entry.mode === "040000") nodeState = {
276
276
  kind: "directory",
277
277
  overlay: {
278
278
  addedEntries: /* @__PURE__ */ new Map(),
@@ -326,7 +326,7 @@ function buildAddedModes(state, dirNode) {
326
326
  return addedModes;
327
327
  }
328
328
  function workdirNodeMode(node) {
329
- if (node.state.kind === "directory") return "40000";
329
+ if (node.state.kind === "directory") return "040000";
330
330
  if (node.state.kind === "symlink") return "120000";
331
331
  return node.state.mode;
332
332
  }
@@ -79,7 +79,7 @@ function statNode(source, node, path) {
79
79
  function statDirectoryNode(node) {
80
80
  return {
81
81
  kind: "tree",
82
- mode: "40000",
82
+ mode: "040000",
83
83
  size: 0,
84
84
  hash: node.origin.kind === "repo-tree" ? node.origin.hash : null
85
85
  };
@@ -101,7 +101,7 @@ function compileChildEntry(writeSource, readSource, state, child) {
101
101
  const originHash = node.origin.kind === "repo-tree" ? node.origin.hash : null;
102
102
  return {
103
103
  entry: {
104
- mode: "40000",
104
+ mode: "040000",
105
105
  name: child.name,
106
106
  hash: newHash
107
107
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nano-git",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "files": [
5
5
  "dist"
6
6
  ],