nano-git 0.2.0 → 0.2.1

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.
@@ -120,5 +120,75 @@ declare class TransactionError extends GitError {
120
120
  declare class PreconditionCheckError extends GitError {
121
121
  constructor(message: string);
122
122
  }
123
+ /**
124
+ * 虚拟路径未找到错误
125
+ *
126
+ * 当操作的路径在 session 中不存在时抛出。
127
+ */
128
+ declare class VirtualPathNotFoundError extends GitError {
129
+ /** 不存在的路径 */
130
+ path: string;
131
+ constructor(path: string, message?: string);
132
+ }
133
+ /**
134
+ * 虚拟路径已存在错误
135
+ *
136
+ * 当创建的路径已在 session 中存在时抛出。
137
+ */
138
+ declare class VirtualPathAlreadyExistsError extends GitError {
139
+ /** 已存在的路径 */
140
+ path: string;
141
+ constructor(path: string, message?: string);
142
+ }
143
+ /**
144
+ * 非目录错误
145
+ *
146
+ * 当期望路径为目录但实际不是时抛出。
147
+ */
148
+ declare class VirtualNotDirectoryError extends GitError {
149
+ /** 路径 */
150
+ path: string;
151
+ constructor(path: string, message?: string);
152
+ }
153
+ /**
154
+ * 非文件错误
155
+ *
156
+ * 当期望路径为文件但实际不是时抛出。
157
+ */
158
+ declare class VirtualNotFileError extends GitError {
159
+ /** 路径 */
160
+ path: string;
161
+ constructor(path: string, message?: string);
162
+ }
163
+ /**
164
+ * 非符号链接错误
165
+ *
166
+ * 当期望路径为符号链接但实际不是时抛出。
167
+ */
168
+ declare class VirtualNotSymlinkError extends GitError {
169
+ /** 路径 */
170
+ path: string;
171
+ constructor(path: string, message?: string);
172
+ }
173
+ /**
174
+ * 虚拟工作目录 origin 不可用错误
175
+ *
176
+ * 当操作的路径在 repo 中的 origin 对象缺失时抛出(弱保证场景)。
177
+ */
178
+ declare class VirtualOriginUnavailableError extends GitError {
179
+ /** 路径 */
180
+ path: string;
181
+ constructor(path: string, message?: string);
182
+ }
183
+ /**
184
+ * 虚拟工作目录 revert 不支持错误
185
+ *
186
+ * 当对纯新建节点调用 revert 时抛出,因为其没有可恢复的 origin。
187
+ */
188
+ declare class VirtualRevertNotSupportedError extends GitError {
189
+ /** 路径 */
190
+ path: string;
191
+ constructor(path: string, message?: string);
192
+ }
123
193
  //#endregion
124
- export { CircularReferenceError, DeltaError, GitError, InvalidObjectError, InvalidPackError, InvalidSHA1Error, ObjectNotFoundError, PackError, PackIndexError, PreconditionCheckError, RefNotFoundError, RepositoryError, TransactionError };
194
+ export { CircularReferenceError, DeltaError, GitError, InvalidObjectError, InvalidPackError, InvalidSHA1Error, ObjectNotFoundError, PackError, PackIndexError, PreconditionCheckError, RefNotFoundError, RepositoryError, TransactionError, VirtualNotDirectoryError, VirtualNotFileError, VirtualNotSymlinkError, VirtualOriginUnavailableError, VirtualPathAlreadyExistsError, VirtualPathNotFoundError, VirtualRevertNotSupportedError };
@@ -164,5 +164,103 @@ var PreconditionCheckError = class extends GitError {
164
164
  this.name = "PreconditionCheckError";
165
165
  }
166
166
  };
167
+ /**
168
+ * 虚拟路径未找到错误
169
+ *
170
+ * 当操作的路径在 session 中不存在时抛出。
171
+ */
172
+ var VirtualPathNotFoundError = class extends GitError {
173
+ /** 不存在的路径 */
174
+ path;
175
+ constructor(path, message) {
176
+ super(message ?? `Virtual path not found: ${path}`);
177
+ this.name = "VirtualPathNotFoundError";
178
+ this.path = path;
179
+ }
180
+ };
181
+ /**
182
+ * 虚拟路径已存在错误
183
+ *
184
+ * 当创建的路径已在 session 中存在时抛出。
185
+ */
186
+ var VirtualPathAlreadyExistsError = class extends GitError {
187
+ /** 已存在的路径 */
188
+ path;
189
+ constructor(path, message) {
190
+ super(message ?? `Virtual path already exists: ${path}`);
191
+ this.name = "VirtualPathAlreadyExistsError";
192
+ this.path = path;
193
+ }
194
+ };
195
+ /**
196
+ * 非目录错误
197
+ *
198
+ * 当期望路径为目录但实际不是时抛出。
199
+ */
200
+ var VirtualNotDirectoryError = class extends GitError {
201
+ /** 路径 */
202
+ path;
203
+ constructor(path, message) {
204
+ super(message ?? `Virtual path is not a directory: ${path}`);
205
+ this.name = "VirtualNotDirectoryError";
206
+ this.path = path;
207
+ }
208
+ };
209
+ /**
210
+ * 非文件错误
211
+ *
212
+ * 当期望路径为文件但实际不是时抛出。
213
+ */
214
+ var VirtualNotFileError = class extends GitError {
215
+ /** 路径 */
216
+ path;
217
+ constructor(path, message) {
218
+ super(message ?? `Virtual path is not a file: ${path}`);
219
+ this.name = "VirtualNotFileError";
220
+ this.path = path;
221
+ }
222
+ };
223
+ /**
224
+ * 非符号链接错误
225
+ *
226
+ * 当期望路径为符号链接但实际不是时抛出。
227
+ */
228
+ var VirtualNotSymlinkError = class extends GitError {
229
+ /** 路径 */
230
+ path;
231
+ constructor(path, message) {
232
+ super(message ?? `Virtual path is not a symlink: ${path}`);
233
+ this.name = "VirtualNotSymlinkError";
234
+ this.path = path;
235
+ }
236
+ };
237
+ /**
238
+ * 虚拟工作目录 origin 不可用错误
239
+ *
240
+ * 当操作的路径在 repo 中的 origin 对象缺失时抛出(弱保证场景)。
241
+ */
242
+ var VirtualOriginUnavailableError = class extends GitError {
243
+ /** 路径 */
244
+ path;
245
+ constructor(path, message) {
246
+ super(message ?? `Virtual origin unavailable for: ${path}`);
247
+ this.name = "VirtualOriginUnavailableError";
248
+ this.path = path;
249
+ }
250
+ };
251
+ /**
252
+ * 虚拟工作目录 revert 不支持错误
253
+ *
254
+ * 当对纯新建节点调用 revert 时抛出,因为其没有可恢复的 origin。
255
+ */
256
+ var VirtualRevertNotSupportedError = class extends GitError {
257
+ /** 路径 */
258
+ path;
259
+ constructor(path, message) {
260
+ super(message ?? `Virtual revert not supported for purely new path: ${path}`);
261
+ this.name = "VirtualRevertNotSupportedError";
262
+ this.path = path;
263
+ }
264
+ };
167
265
  //#endregion
168
- export { CircularReferenceError, DeltaError, GitError, InvalidObjectError, InvalidPackError, InvalidSHA1Error, ObjectNotFoundError, PackError, PackIndexError, PreconditionCheckError, RefNotFoundError, RepositoryError, TransactionError };
266
+ export { CircularReferenceError, DeltaError, GitError, InvalidObjectError, InvalidPackError, InvalidSHA1Error, ObjectNotFoundError, PackError, PackIndexError, PreconditionCheckError, RefNotFoundError, RepositoryError, TransactionError, VirtualNotDirectoryError, VirtualNotFileError, VirtualNotSymlinkError, VirtualOriginUnavailableError, VirtualPathAlreadyExistsError, VirtualPathNotFoundError, VirtualRevertNotSupportedError };
package/dist/errors.d.mts CHANGED
@@ -1,2 +1,2 @@
1
- import { CircularReferenceError, DeltaError, GitError, InvalidObjectError, InvalidPackError, InvalidSHA1Error, ObjectNotFoundError, PackError, PackIndexError, PreconditionCheckError, RefNotFoundError, RepositoryError, TransactionError } from "./core/errors.mjs";
2
- export { CircularReferenceError, DeltaError, GitError, InvalidObjectError, InvalidPackError, InvalidSHA1Error, ObjectNotFoundError, PackError, PackIndexError, PreconditionCheckError, RefNotFoundError, RepositoryError, TransactionError };
1
+ import { CircularReferenceError, DeltaError, GitError, InvalidObjectError, InvalidPackError, InvalidSHA1Error, ObjectNotFoundError, PackError, PackIndexError, PreconditionCheckError, RefNotFoundError, RepositoryError, TransactionError, VirtualNotDirectoryError, VirtualNotFileError, VirtualNotSymlinkError, VirtualOriginUnavailableError, VirtualPathAlreadyExistsError, VirtualPathNotFoundError, VirtualRevertNotSupportedError } from "./core/errors.mjs";
2
+ export { CircularReferenceError, DeltaError, GitError, InvalidObjectError, InvalidPackError, InvalidSHA1Error, ObjectNotFoundError, PackError, PackIndexError, PreconditionCheckError, RefNotFoundError, RepositoryError, TransactionError, VirtualNotDirectoryError, VirtualNotFileError, VirtualNotSymlinkError, VirtualOriginUnavailableError, VirtualPathAlreadyExistsError, VirtualPathNotFoundError, VirtualRevertNotSupportedError };
package/dist/errors.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { CircularReferenceError, DeltaError, GitError, InvalidObjectError, InvalidPackError, InvalidSHA1Error, ObjectNotFoundError, PackError, PackIndexError, PreconditionCheckError, RefNotFoundError, RepositoryError, TransactionError } from "./core/errors.mjs";
2
- export { CircularReferenceError, DeltaError, GitError, InvalidObjectError, InvalidPackError, InvalidSHA1Error, ObjectNotFoundError, PackError, PackIndexError, PreconditionCheckError, RefNotFoundError, RepositoryError, TransactionError };
1
+ import { CircularReferenceError, DeltaError, GitError, InvalidObjectError, InvalidPackError, InvalidSHA1Error, ObjectNotFoundError, PackError, PackIndexError, PreconditionCheckError, RefNotFoundError, RepositoryError, TransactionError, VirtualNotDirectoryError, VirtualNotFileError, VirtualNotSymlinkError, VirtualOriginUnavailableError, VirtualPathAlreadyExistsError, VirtualPathNotFoundError, VirtualRevertNotSupportedError } from "./core/errors.mjs";
2
+ export { CircularReferenceError, DeltaError, GitError, InvalidObjectError, InvalidPackError, InvalidSHA1Error, ObjectNotFoundError, PackError, PackIndexError, PreconditionCheckError, RefNotFoundError, RepositoryError, TransactionError, VirtualNotDirectoryError, VirtualNotFileError, VirtualNotSymlinkError, VirtualOriginUnavailableError, VirtualPathAlreadyExistsError, VirtualPathNotFoundError, VirtualRevertNotSupportedError };
@@ -0,0 +1,3 @@
1
+ import { CommitWalkOrder, LogEntry, LogWalkOptions } from "./types.mjs";
2
+ import { walkLogEntries } from "./walk.mjs";
3
+ export { type CommitWalkOrder, type LogEntry, type LogWalkOptions, walkLogEntries };
@@ -0,0 +1,2 @@
1
+ import { walkLogEntries } from "./walk.mjs";
2
+ export { walkLogEntries };
@@ -0,0 +1,71 @@
1
+ import { GitCommit, SHA1 } from "../core/types.mjs";
2
+
3
+ //#region src/log/types.d.ts
4
+ /** 单条提交日志条目 */
5
+ interface LogEntry {
6
+ /** 提交哈希 */
7
+ readonly hash: SHA1;
8
+ /** 解码后的提交对象 */
9
+ readonly commit: GitCommit;
10
+ }
11
+ /** 提交遍历排序策略 */
12
+ type CommitWalkOrder = "date" | "topo";
13
+ /** 日志遍历选项 */
14
+ interface LogWalkOptions {
15
+ /**
16
+ * 遍历起点哈希列表。
17
+ *
18
+ * 为空时立即返回空结果。
19
+ * 调用方应自行将 ref 名称(如 HEAD、branch names)解析为 SHA1。
20
+ */
21
+ readonly from?: SHA1[];
22
+ /**
23
+ * 排除哈希列表(及其所有祖先)。
24
+ *
25
+ * 等价于 `git log <from> ^<exclude>` 或 `<exclude>..<from>` 范围语法。
26
+ * 被排除的提交及其全部祖先不会出现在结果中。
27
+ */
28
+ readonly exclude?: SHA1[];
29
+ /**
30
+ * 最多输出的提交数量。
31
+ *
32
+ * 等价于 `git log --max-count=<n>`。
33
+ */
34
+ readonly maxCount?: number;
35
+ /**
36
+ * 跳过开头 N 个提交再输出。
37
+ *
38
+ * 等价于 `git log --skip=<n>`。
39
+ */
40
+ readonly skip?: number;
41
+ /**
42
+ * 提交排序策略。
43
+ *
44
+ * - `"date"`(默认):按提交时间降序。时间相同时不保证严格拓扑序,
45
+ * 但在实践中 parent 的 timestamp 通常 ≤ child,结果与 git log 默认一致。
46
+ * - `"topo"`:严格拓扑排序,所有子提交在父提交之前输出。
47
+ * 同层级的提交按时间戳降序。需要预先收集全部提交,额外开销较大。
48
+ */
49
+ readonly order?: CommitWalkOrder;
50
+ /**
51
+ * 仅输出此 Unix 时间戳(含)之后的提交。
52
+ *
53
+ * 等价于 `git log --since=<timestamp>`。
54
+ */
55
+ readonly since?: number;
56
+ /**
57
+ * 仅输出此 Unix 时间戳(含)之前的提交。
58
+ *
59
+ * 等价于 `git log --until=<timestamp>`。
60
+ */
61
+ readonly until?: number;
62
+ /**
63
+ * 仅沿第一条父链行走。
64
+ *
65
+ * 启用后只追踪每个提交的第一个 parent,忽略 merge 的其他分支。
66
+ * 等价于 `git log --first-parent`。
67
+ */
68
+ readonly firstParent?: boolean;
69
+ }
70
+ //#endregion
71
+ export { CommitWalkOrder, LogEntry, LogWalkOptions };
@@ -0,0 +1,37 @@
1
+ import { ObjectSource } from "../core/types/odb.mjs";
2
+ import { LogEntry, LogWalkOptions } from "./types.mjs";
3
+
4
+ //#region src/log/walk.d.ts
5
+ /**
6
+ * 遍历提交历史日志
7
+ *
8
+ * 从指定的起点哈希出发,沿 parent 链回溯,按指定排序策略输出提交。
9
+ * 不涉及 ref 解析——调用方需自行将 ref 名称解析为 SHA1。
10
+ *
11
+ * @param source - 对象源(通常是 `Repository.objects`)
12
+ * @param options - 遍历选项
13
+ * @returns 按序输出的提交日志条目生成器
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * const headHash = resolveRefHash(repo.refs, "HEAD")!;
18
+ * for (const entry of walkLogEntries(repo.objects, { from: [headHash], maxCount: 5 })) {
19
+ * console.log(entry.hash, entry.commit.subject);
20
+ * }
21
+ * ```
22
+ *
23
+ * @example
24
+ * ```ts
25
+ * // 等价于 git log --oneline --since=1600000000 --until=1700000000
26
+ * for (const entry of walkLogEntries(repo.objects, {
27
+ * from: [headHash],
28
+ * since: 1600000000,
29
+ * until: 1700000000,
30
+ * })) {
31
+ * console.log(entry.hash.slice(0, 7), entry.commit.message.split("\n")[0]);
32
+ * }
33
+ * ```
34
+ */
35
+ declare function walkLogEntries(source: ObjectSource, options?: LogWalkOptions): Generator<LogEntry, void, undefined>;
36
+ //#endregion
37
+ export { walkLogEntries };
@@ -0,0 +1,274 @@
1
+ import { tryReadObject } from "../objects/raw.mjs";
2
+ //#region src/log/walk.ts
3
+ /**
4
+ * 提交日志遍历核心实现
5
+ *
6
+ * 提供 Generator 风格的 `walkLogEntries`,支持按提交时间降序
7
+ * 或严格拓扑序遍历提交历史。
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * // 从 HEAD 开始遍历最近 10 条提交
12
+ * const headHash = resolveRefHash(repo.refs, "HEAD")!;
13
+ * for (const entry of walkLogEntries(repo.objects, { from: [headHash], maxCount: 10 })) {
14
+ * console.log(entry.hash, entry.commit.message);
15
+ * }
16
+ *
17
+ * // 等价于 git log main..feature
18
+ * const featureHash = resolveRefHash(repo.refs, "refs/heads/feature")!;
19
+ * const mainHash = resolveRefHash(repo.refs, "refs/heads/main")!;
20
+ * for (const entry of walkLogEntries(repo.objects, { from: [featureHash], exclude: [mainHash] })) {
21
+ * console.log(entry.hash, entry.commit.message);
22
+ * }
23
+ * ```
24
+ */
25
+ var MaxHeapByTimestamp = class {
26
+ heap = [];
27
+ get size() {
28
+ return this.heap.length;
29
+ }
30
+ push(entry) {
31
+ this.heap.push(entry);
32
+ this.siftUp(this.heap.length - 1);
33
+ }
34
+ pop() {
35
+ if (this.heap.length === 0) return void 0;
36
+ const top = this.heap[0];
37
+ const bottom = this.heap.pop();
38
+ if (this.heap.length > 0) {
39
+ this.heap[0] = bottom;
40
+ this.siftDown(0);
41
+ }
42
+ return top;
43
+ }
44
+ siftUp(idx) {
45
+ const heap = this.heap;
46
+ while (idx > 0) {
47
+ const parent = idx - 1 >> 1;
48
+ if (heap[parent].commit.committer.timestamp >= heap[idx].commit.committer.timestamp) break;
49
+ [heap[parent], heap[idx]] = [heap[idx], heap[parent]];
50
+ idx = parent;
51
+ }
52
+ }
53
+ siftDown(idx) {
54
+ const heap = this.heap;
55
+ const end = heap.length;
56
+ while (true) {
57
+ let largest = idx;
58
+ const left = (idx << 1) + 1;
59
+ const right = left + 1;
60
+ if (left < end && heap[left].commit.committer.timestamp > heap[largest].commit.committer.timestamp) largest = left;
61
+ if (right < end && heap[right].commit.committer.timestamp > heap[largest].commit.committer.timestamp) largest = right;
62
+ if (largest === idx) break;
63
+ [heap[idx], heap[largest]] = [heap[largest], heap[idx]];
64
+ idx = largest;
65
+ }
66
+ }
67
+ };
68
+ /**
69
+ * 递归标记排除提交及其所有祖先
70
+ */
71
+ function markExcluded(source, hash, excluded) {
72
+ const stack = [hash];
73
+ while (stack.length > 0) {
74
+ const current = stack.pop();
75
+ if (excluded.has(current)) continue;
76
+ excluded.add(current);
77
+ const obj = tryReadObject(source, current);
78
+ if (obj?.type === "commit") {
79
+ for (const parent of obj.parents) if (!excluded.has(parent)) stack.push(parent);
80
+ }
81
+ }
82
+ }
83
+ /**
84
+ * 按提交时间降序遍历提交历史
85
+ *
86
+ * 使用最大堆按时间戳排序,每次弹出最新提交后将其 parent 入堆。
87
+ * 可配合 firstParent、since、until、skip、maxCount 等过滤条件。
88
+ */
89
+ function* walkByDate(source, from, excluded, firstParent, skip, maxCount, since, until) {
90
+ const queue = new MaxHeapByTimestamp();
91
+ const seen = /* @__PURE__ */ new Set();
92
+ const visited = /* @__PURE__ */ new Set();
93
+ for (const hash of from) {
94
+ if (excluded.has(hash) || seen.has(hash)) continue;
95
+ const obj = tryReadObject(source, hash);
96
+ if (obj?.type !== "commit") continue;
97
+ seen.add(hash);
98
+ queue.push({
99
+ hash,
100
+ commit: obj
101
+ });
102
+ }
103
+ let skipped = 0;
104
+ let emitted = 0;
105
+ while (queue.size > 0) {
106
+ if (maxCount !== void 0 && emitted >= maxCount) break;
107
+ const entry = queue.pop();
108
+ if (visited.has(entry.hash)) continue;
109
+ if (excluded.has(entry.hash)) continue;
110
+ if (since !== void 0 && entry.commit.committer.timestamp < since) continue;
111
+ if (until !== void 0 && entry.commit.committer.timestamp > until) {
112
+ enqueueParents(source, entry.commit.parents, firstParent, excluded, seen, visited, queue);
113
+ continue;
114
+ }
115
+ if (skipped < skip) {
116
+ skipped++;
117
+ visited.add(entry.hash);
118
+ enqueueParents(source, entry.commit.parents, firstParent, excluded, seen, visited, queue);
119
+ continue;
120
+ }
121
+ visited.add(entry.hash);
122
+ emitted++;
123
+ yield {
124
+ hash: entry.hash,
125
+ commit: entry.commit
126
+ };
127
+ enqueueParents(source, entry.commit.parents, firstParent, excluded, seen, visited, queue);
128
+ }
129
+ }
130
+ /**
131
+ * 将提交的 parent 批量入堆
132
+ */
133
+ function enqueueParents(source, parents, firstParent, excluded, seen, visited, queue) {
134
+ const targetParents = firstParent && parents.length > 0 ? [parents[0]] : parents;
135
+ for (const parentHash of targetParents) {
136
+ if (seen.has(parentHash) || excluded.has(parentHash)) continue;
137
+ seen.add(parentHash);
138
+ const obj = tryReadObject(source, parentHash);
139
+ if (obj?.type !== "commit") continue;
140
+ queue.push({
141
+ hash: parentHash,
142
+ commit: obj
143
+ });
144
+ }
145
+ }
146
+ /**
147
+ * 生成严格拓扑序(子提交先于父提交)的提交迭代器
148
+ *
149
+ * 实现 Kahn 算法:
150
+ * 1. 收集所有起点可达的提交,构建 parent→children 映射
151
+ * 2. 统计每个提交的未输出子提交数(childCount)
152
+ * 3. 将 childCount === 0 的提交入堆(同层按时间戳降序)
153
+ * 4. 每次弹出堆顶输出,递减其 parent 的 childCount
154
+ * 5. parent 的 childCount 归零时入堆
155
+ */
156
+ function* walkTopo(source, from, excluded, firstParent, skip, maxCount, since, until) {
157
+ const commits = /* @__PURE__ */ new Map();
158
+ const children = /* @__PURE__ */ new Map();
159
+ const stack = [...from];
160
+ while (stack.length > 0) {
161
+ const hash = stack.pop();
162
+ if (commits.has(hash) || excluded.has(hash)) continue;
163
+ const obj = tryReadObject(source, hash);
164
+ if (obj?.type !== "commit") continue;
165
+ commits.set(hash, obj);
166
+ if (!children.has(hash)) children.set(hash, []);
167
+ const targetParents = firstParent && obj.parents.length > 0 ? [obj.parents[0]] : obj.parents;
168
+ for (const parentHash of targetParents) {
169
+ if (commits.has(parentHash) || excluded.has(parentHash)) continue;
170
+ if (!children.has(parentHash)) children.set(parentHash, []);
171
+ children.get(parentHash).push(hash);
172
+ stack.push(parentHash);
173
+ }
174
+ }
175
+ if (commits.size === 0) return;
176
+ const childCount = /* @__PURE__ */ new Map();
177
+ const pq = new MaxHeapByTimestamp();
178
+ for (const hash of commits.keys()) {
179
+ const count = children.get(hash)?.length ?? 0;
180
+ childCount.set(hash, count);
181
+ if (count === 0) pq.push({
182
+ hash,
183
+ commit: commits.get(hash)
184
+ });
185
+ }
186
+ let skipped = 0;
187
+ let emitted = 0;
188
+ while (pq.size > 0) {
189
+ if (maxCount !== void 0 && emitted >= maxCount) break;
190
+ const entry = pq.pop();
191
+ if (since !== void 0 && entry.commit.committer.timestamp < since) {
192
+ decrementParentCounts(entry.commit.parents, firstParent, childCount, commits, pq);
193
+ continue;
194
+ }
195
+ if (until !== void 0 && entry.commit.committer.timestamp > until) {
196
+ decrementParentCounts(entry.commit.parents, firstParent, childCount, commits, pq);
197
+ continue;
198
+ }
199
+ if (skipped < skip) {
200
+ skipped++;
201
+ decrementParentCounts(entry.commit.parents, firstParent, childCount, commits, pq);
202
+ continue;
203
+ }
204
+ emitted++;
205
+ yield {
206
+ hash: entry.hash,
207
+ commit: entry.commit
208
+ };
209
+ decrementParentCounts(entry.commit.parents, firstParent, childCount, commits, pq);
210
+ }
211
+ }
212
+ /**
213
+ * 递减 parent 的 childCount,归零时入堆
214
+ */
215
+ function decrementParentCounts(parents, firstParent, childCount, commits, pq) {
216
+ const targetParents = firstParent && parents.length > 0 ? [parents[0]] : parents;
217
+ for (const parentHash of targetParents) {
218
+ const count = childCount.get(parentHash);
219
+ if (count === void 0) continue;
220
+ const newCount = count - 1;
221
+ childCount.set(parentHash, newCount);
222
+ if (newCount === 0) {
223
+ const parentCommit = commits.get(parentHash);
224
+ if (parentCommit) pq.push({
225
+ hash: parentHash,
226
+ commit: parentCommit
227
+ });
228
+ }
229
+ }
230
+ }
231
+ /**
232
+ * 遍历提交历史日志
233
+ *
234
+ * 从指定的起点哈希出发,沿 parent 链回溯,按指定排序策略输出提交。
235
+ * 不涉及 ref 解析——调用方需自行将 ref 名称解析为 SHA1。
236
+ *
237
+ * @param source - 对象源(通常是 `Repository.objects`)
238
+ * @param options - 遍历选项
239
+ * @returns 按序输出的提交日志条目生成器
240
+ *
241
+ * @example
242
+ * ```ts
243
+ * const headHash = resolveRefHash(repo.refs, "HEAD")!;
244
+ * for (const entry of walkLogEntries(repo.objects, { from: [headHash], maxCount: 5 })) {
245
+ * console.log(entry.hash, entry.commit.subject);
246
+ * }
247
+ * ```
248
+ *
249
+ * @example
250
+ * ```ts
251
+ * // 等价于 git log --oneline --since=1600000000 --until=1700000000
252
+ * for (const entry of walkLogEntries(repo.objects, {
253
+ * from: [headHash],
254
+ * since: 1600000000,
255
+ * until: 1700000000,
256
+ * })) {
257
+ * console.log(entry.hash.slice(0, 7), entry.commit.message.split("\n")[0]);
258
+ * }
259
+ * ```
260
+ */
261
+ function walkLogEntries(source, options = {}) {
262
+ const { from = [], exclude = [], skip: skipCount = 0, order = "date", since, until, firstParent = false, maxCount } = options;
263
+ if (from.length === 0) return emptyGenerator();
264
+ const excluded = /* @__PURE__ */ new Set();
265
+ for (const hash of exclude) markExcluded(source, hash, excluded);
266
+ if (order === "topo") return walkTopo(source, from, excluded, firstParent, skipCount, maxCount, since, until);
267
+ return walkByDate(source, from, excluded, firstParent, skipCount, maxCount, since, until);
268
+ }
269
+ /** 返回一个空的 Generator(不产生任何值,直接 done) */
270
+ function emptyGenerator() {
271
+ return [][Symbol.iterator]();
272
+ }
273
+ //#endregion
274
+ export { walkLogEntries };
@@ -0,0 +1,63 @@
1
+ //#region src/workdir/change-log.ts
2
+ /**
3
+ * 创建空的变更日志
4
+ *
5
+ * @example
6
+ * ```ts
7
+ * const log = createVirtualChangeLog();
8
+ * log.append({ op: "add", path: "a.txt" });
9
+ * expect(log.toVirtualChanges()).toEqual([{ path: "a.txt", type: "add" }]);
10
+ * ```
11
+ */
12
+ function createVirtualChangeLog() {
13
+ const records = [];
14
+ return {
15
+ append(record) {
16
+ records.push(record);
17
+ },
18
+ clear() {
19
+ records.length = 0;
20
+ },
21
+ snapshot() {
22
+ return records.slice();
23
+ },
24
+ toVirtualChanges() {
25
+ const out = [];
26
+ for (const record of records) {
27
+ const mapped = mapInternalToVirtual(record);
28
+ if (mapped !== null) out.push(mapped);
29
+ }
30
+ return out;
31
+ }
32
+ };
33
+ }
34
+ function mapInternalToVirtual(record) {
35
+ switch (record.op) {
36
+ case "add": return {
37
+ path: record.path,
38
+ type: "add"
39
+ };
40
+ case "modify": return {
41
+ path: record.path,
42
+ type: "modify"
43
+ };
44
+ case "delete": return {
45
+ path: record.path,
46
+ type: "delete"
47
+ };
48
+ case "rename": return {
49
+ path: record.to,
50
+ type: "rename",
51
+ oldPath: record.from
52
+ };
53
+ case "copy": return {
54
+ path: record.to,
55
+ type: "copy",
56
+ oldPath: record.from
57
+ };
58
+ case "revert": return null;
59
+ default: return record;
60
+ }
61
+ }
62
+ //#endregion
63
+ export { createVirtualChangeLog };