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; commitFile(storageDir: string, objectKey: string, message: string): Promise; getLog( storageDir: string, objectKey: string, limit?: number, ): Promise>; } 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 { 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 []; } }, }; }