nexus/server/src/services/git-file-service.ts
Nexus Dev dae0e3a3be feat(25-06): merge git file service and history endpoint from worktree
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>
2026-04-04 03:55:48 +00:00

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 [];
}
},
};
}