feat(filelib): 项目新增修改历史 tab 并隐藏 version hash id

This commit is contained in:
2026-07-27 21:09:10 +08:00
parent 8c26c42e0c
commit 1dab83f9db
8 changed files with 234 additions and 9 deletions
+12 -1
View File
@@ -10,7 +10,7 @@
import { FileLibError } from "./model.js";
import { FILE_LIB_AUDIT_ACTIONS, writeFileLibAudit } from "./audit.js";
import type { CommitResult, FileEntry, VersionInfo, VersionStore } from "./versionStore.js";
import type { CommitResult, FileEntry, VersionInfo, VersionStore, ProjectCommitInfo } from "./versionStore.js";
import type { AccessDeps, FileLibActor } from "./treeService.js";
import { requireAccessInTx } from "./treeService.js";
import type { FileLibNode, PrismaClient } from "@prisma/client";
@@ -228,6 +228,17 @@ export async function fileHistory(
return deps.versionStore.history(project.storageDir, filePath, limit);
}
/** 项目级提交历史(所有文件),VIEW 即可访问。 */
export async function projectHistory(
deps: FileDeps,
actor: FileLibActor,
projectId: string,
limit?: number,
): Promise<readonly ProjectCommitInfo[]> {
const project = await requireProject(deps, actor, projectId, "VIEW");
return deps.versionStore.projectHistory(project.storageDir, limit);
}
export async function diffFile(
deps: FileDeps,
actor: FileLibActor,
@@ -20,6 +20,7 @@ import type {
CommitRequest,
CommitResult,
FileEntry,
ProjectCommitInfo,
VersionId,
VersionInfo,
VersionStore,
@@ -404,5 +405,39 @@ export function createGitVersionStore(): VersionStore {
}
return out; // git log 已是新→旧
},
async projectHistory(projectDir, limit) {
await requireRepo(projectDir);
// %x1e 放在 format 开头作为每条记录的分隔符;--name-only 的文件列表跟在 format 行之后。
const args = ["log", "--format=%x1e%H%x1f%an%x1f%aI%x1f%s", "--name-only"];
if (limit !== undefined) args.push(`-${limit}`);
const res = await runGit(projectDir, args);
if (res.code !== 0) return []; // 空仓库
const out: ProjectCommitInfo[] = [];
// split 按 \x1e 分块,第一个空块跳过。
for (const block of res.stdout.toString("utf8").split("\x1e")) {
const trimmed = block.trim();
if (trimmed === "") continue;
const lines = trimmed.split("\n");
const header = lines[0];
if (header === undefined) continue;
const [version, author, committedAt, message] = header.split("\x1f");
if (version === undefined) continue;
// header 之后的非空行是受影响的文件路径。
// git 对含特殊字符的路径加双引号,去掉外层引号即可。
const files = lines.slice(1)
.map((l) => l.trim())
.filter((l) => l !== "")
.map((l) => (l.startsWith('"') && l.endsWith('"') ? l.slice(1, -1) : l));
out.push({
version,
message: message ?? "",
author: author === undefined || author === "" ? undefined : author,
committedAt: committedAt ?? "",
files,
});
}
return out;
},
};
}
+31
View File
@@ -45,6 +45,16 @@ export interface VersionInfo {
readonly committedAt: string;
}
/** 项目级提交历史条目(包含受影响文件路径)。 */
export interface ProjectCommitInfo {
readonly version: VersionId;
readonly message: string;
readonly author: string | undefined;
readonly committedAt: string;
/** 本次提交修改的文件路径列表。 */
readonly files: readonly string[];
}
export interface FileEntry {
readonly path: string;
readonly size: number;
@@ -65,6 +75,8 @@ export interface VersionStore {
): Promise<CommitResult>;
diff(projectDir: string, filePath: string, from: VersionId, to: VersionId): Promise<string>;
history(projectDir: string, filePath: string, limit?: number): Promise<VersionInfo[]>;
/** 项目级提交历史(所有文件),新→旧排列。 */
projectHistory(projectDir: string, limit?: number): Promise<ProjectCommitInfo[]>;
}
interface StoredVersion {
@@ -300,5 +312,24 @@ export function createInMemoryVersionStore(persistPath?: string): VersionStore {
const ordered = infos.reverse();
return limit !== undefined ? ordered.slice(0, limit) : ordered;
},
async projectHistory(projectDir, limit) {
const repo = requireRepo(projectDir);
// 汇集所有文件的所有版本,按提交时间新→旧排序。
const all: ProjectCommitInfo[] = [];
for (const [filePath, chain] of repo.files) {
for (const v of chain) {
all.push({
version: v.version,
message: v.message,
author: v.author,
committedAt: v.committedAt,
files: [filePath],
});
}
}
all.sort((a, b) => b.committedAt.localeCompare(a.committedAt));
return limit !== undefined ? all.slice(0, limit) : all;
},
};
}
+17
View File
@@ -10,6 +10,7 @@ import {
diffFile,
fileHistory,
listFiles,
projectHistory,
readFile,
readFileRaw,
type FileContentEncoding,
@@ -183,6 +184,22 @@ export async function registerFileRoutes(
}
});
app.get("/database/api/projects/:id/history", async (request, reply) => {
const actor = await actorOrNull(request, reply, deps);
if (actor === null) return reply;
try {
const { id } = request.params as { id: string };
const rawLimit = (request.query as { limit?: string }).limit;
const limit = rawLimit === undefined ? undefined : Number.parseInt(rawLimit, 10);
if (limit !== undefined && (!Number.isSafeInteger(limit) || limit <= 0)) {
throw new FileLibError(400, "invalid_request", "limit must be a positive integer");
}
return { history: await projectHistory(fileDeps, actor, id, limit) };
} catch (error) {
return sendRouteError(reply, error);
}
});
/* ------------------------------------------------------------ 导出(D10 异步) */
app.post("/database/api/projects/:id/exports", async (request, reply) => {