Adds gitFileService with commitFile/getLog, wires git commits into upload flow, adds GET /files/:fileId/history endpoint, and exports ChatFileHistoryEntry type. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
101 lines
2.7 KiB
TypeScript
101 lines
2.7 KiB
TypeScript
import { execFile as execFileCb } from "node:child_process";
|
|
import { promisify } from "node:util";
|
|
import { existsSync } from "node:fs";
|
|
import path from "node:path";
|
|
|
|
const execFile = promisify(execFileCb);
|
|
|
|
export interface GitFileService {
|
|
ensureRepo(storageDir: string): Promise<void>;
|
|
commitFile(storageDir: string, objectKey: string, message: string): Promise<string | null>;
|
|
getLog(
|
|
storageDir: string,
|
|
objectKey: string,
|
|
limit?: number,
|
|
): Promise<Array<{ hash: string; date: string; message: string; author: string }>>;
|
|
}
|
|
|
|
export function gitFileService(): GitFileService {
|
|
async function git(
|
|
cwd: string,
|
|
args: string[],
|
|
): Promise<{ stdout: string; stderr: string }> {
|
|
return execFile("git", args, { cwd, timeout: 10000 });
|
|
}
|
|
|
|
return {
|
|
async ensureRepo(storageDir: string) {
|
|
const gitDir = path.join(storageDir, ".git");
|
|
if (existsSync(gitDir)) return;
|
|
await git(storageDir, ["init"]);
|
|
await git(storageDir, ["config", "user.email", "nexus@local"]);
|
|
await git(storageDir, ["config", "user.name", "Nexus File System"]);
|
|
},
|
|
|
|
async commitFile(
|
|
storageDir: string,
|
|
objectKey: string,
|
|
message: string,
|
|
): Promise<string | null> {
|
|
const filePath = path.join(storageDir, objectKey);
|
|
if (!existsSync(filePath)) return null;
|
|
|
|
await this.ensureRepo(storageDir);
|
|
|
|
try {
|
|
await git(storageDir, ["add", "--", objectKey]);
|
|
const { stdout } = await git(storageDir, [
|
|
"commit",
|
|
"-m",
|
|
message,
|
|
"--",
|
|
objectKey,
|
|
]);
|
|
const match = /\[[\w\s]+\s([a-f0-9]+)\]/.exec(stdout);
|
|
return match?.[1] ?? null;
|
|
} catch (err) {
|
|
const errMsg = String(
|
|
(err as { stderr?: string }).stderr ??
|
|
(err as Error).message ??
|
|
"",
|
|
);
|
|
if (
|
|
errMsg.includes("nothing to commit") ||
|
|
errMsg.includes("no changes added")
|
|
) {
|
|
return null;
|
|
}
|
|
throw err;
|
|
}
|
|
},
|
|
|
|
async getLog(storageDir: string, objectKey: string, limit = 50) {
|
|
try {
|
|
await this.ensureRepo(storageDir);
|
|
const { stdout } = await git(storageDir, [
|
|
"log",
|
|
`--max-count=${limit}`,
|
|
"--format=%H|%aI|%s|%an",
|
|
"--",
|
|
objectKey,
|
|
]);
|
|
|
|
return stdout
|
|
.trim()
|
|
.split("\n")
|
|
.filter(Boolean)
|
|
.map((line) => {
|
|
const [hash, date, message, author] = line.split("|");
|
|
return {
|
|
hash: hash!,
|
|
date: date!,
|
|
message: message!,
|
|
author: author!,
|
|
};
|
|
});
|
|
} catch {
|
|
return [];
|
|
}
|
|
},
|
|
};
|
|
}
|