forked from bai/curriculum-project-hub
feat(filelib): 项目新增修改历史 tab 并隐藏 version hash id
This commit is contained in:
@@ -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;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user