import { createServerFn } from "@tanstack/react-start"; import { execFileSync } from "node:child_process"; const REPO_PATH = process.env.GITUI_REPO_PATH || process.cwd(); function git(args: string[]): string { return execFileSync("git", args, { cwd: REPO_PATH, encoding: "utf-8", maxBuffer: 10 * 1024 * 1024, }).replace(/[\r\n]+$/, ""); } function assertSafeRevision(value: string, label: string): void { if (!value || value.startsWith("-")) { throw new Error(`Invalid ${label}: must be non-empty and must not start with "-"`); } } export interface CommitInfo { hash: string; shortHash: string; author: string; date: string; message: string; } export interface GitStatus { staged: Array<{ status: string; path: string }>; unstaged: Array<{ status: string; path: string }>; untracked: string[]; } function parseStatus(raw: string): GitStatus { const staged: GitStatus["staged"] = []; const unstaged: GitStatus["unstaged"] = []; const untracked: string[] = []; const entries = raw.split("\0").filter(Boolean); for (let index = 0; index < entries.length; index++) { const entry = entries[index]; const x = entry[0]; const y = entry[1]; const path = entry.slice(3); if (x === "?") { untracked.push(path); continue; } if (x !== " " && x !== "?") { staged.push({ status: x, path }); } if (y !== " " && y !== "?") { unstaged.push({ status: y, path }); } if ((x === "R" || x === "C") && index + 1 < entries.length) { index++; } } return { staged, unstaged, untracked }; } export const getLog = createServerFn({ method: "POST" }) .validator((d: { count?: number; skip?: number }) => d) .handler(async ({ data }) => { const count = data.count ?? 50; const skip = data.skip ?? 0; const raw = git(["log", "--format=%H%x00%h%x00%an%x00%ai%x00%s", "-n", String(count), `--skip=${skip}`]); if (!raw) return [] as CommitInfo[]; return raw.split("\n").map((line): CommitInfo => { const [hash, shortHash, author, date, message] = line.split("\x00"); return { hash, shortHash, author, date, message }; }); }); export const getTree = createServerFn({ method: "POST" }) .validator((d: { ref?: string }) => d) .handler(async ({ data }) => { const ref = data.ref || "HEAD"; assertSafeRevision(ref, "ref"); const raw = git(["ls-tree", "-r", "--name-only", "--end-of-options", ref]); return raw.split("\n").filter(Boolean); }); export const getFileContent = createServerFn({ method: "POST" }) .validator((d: { ref: string; path: string }) => d) .handler(async ({ data }) => { assertSafeRevision(data.ref, "ref"); assertSafeRevision(data.path, "path"); return git(["show", "--end-of-options", `${data.ref}:${data.path}`]); }); export const getDiff = createServerFn({ method: "POST" }) .validator((d: string) => d) .handler(async ({ data: hash }) => { assertSafeRevision(hash, "hash"); return git(["show", "--format=", "--patch", "--end-of-options", hash]); }); export const getWorkingDiff = createServerFn().handler(async () => { return git(["diff"]); }); export const getStagedDiff = createServerFn().handler(async () => { return git(["diff", "--cached"]); }); export const getStatus = createServerFn().handler(async (): Promise => { return parseStatus(git(["status", "--porcelain=v1", "-z"])); }); export const getBranches = createServerFn().handler(async () => { const current = git(["branch", "--show-current"]); const all = git(["branch", "--format=%(refname:short)"]).split("\n").filter(Boolean); return { current, all }; }); export const getCommitDetail = createServerFn({ method: "POST" }) .validator((d: string) => d) .handler(async ({ data: hash }) => { assertSafeRevision(hash, "hash"); const info = git(["show", "-s", "--format=%H%x00%h%x00%an%x00%ae%x00%ai%x00%B", "--end-of-options", hash]); const stats = git(["show", "--stat", "--format=", "--end-of-options", hash]); const [h, shortHash, author, email, date, ...msgParts] = info.split("\x00"); const message = msgParts.join("\x00"); return { hash: h, shortHash, author, email, date, message, stats }; });