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.
- package/dist/core/errors.d.mts +71 -1
- package/dist/core/errors.mjs +99 -1
- package/dist/errors.d.mts +2 -2
- package/dist/errors.mjs +2 -2
- package/dist/log/index.d.mts +3 -0
- package/dist/log/index.mjs +2 -0
- package/dist/log/types.d.mts +71 -0
- package/dist/log/walk.d.mts +37 -0
- package/dist/log/walk.mjs +274 -0
- package/dist/workdir/change-log.mjs +63 -0
- package/dist/workdir/core.d.mts +166 -0
- package/dist/workdir/core.mjs +2 -0
- package/dist/workdir/ids.mjs +20 -0
- package/dist/workdir/memory-backend.mjs +21 -0
- package/dist/workdir/memory.d.mts +2 -0
- package/dist/workdir/memory.mjs +2 -0
- package/dist/workdir/nodes.mjs +112 -0
- package/dist/workdir/origin.mjs +55 -0
- package/dist/workdir/overlay.mjs +95 -0
- package/dist/workdir/path.mjs +70 -0
- package/dist/workdir/session-internal.mjs +141 -0
- package/dist/workdir/session.d.mts +18 -0
- package/dist/workdir/session.mjs +380 -0
- package/dist/workdir/write-tree.mjs +97 -0
- package/package.json +9 -3
package/dist/core/errors.d.mts
CHANGED
|
@@ -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 };
|
package/dist/core/errors.mjs
CHANGED
|
@@ -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,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 };
|