import { Icon } from "./Icon"; import { useEffect, useState } from "react"; import { post } from "../api"; import { fmtRelative, useT } from "../i18n"; import { Spinner } from "./Activity"; import { confirmDialog, promptDialog, toast } from "../dialogs"; export interface GitFile { path: string; index: string; worktree: string; from?: string; } export interface GitStatus { repo: string; branch: string | null; upstream: string | null; ahead: number; behind: number; files: GitFile[]; } interface Commit { hash: string; author: string; time: number; subject: string; refs: string; } const LABEL: Record = { M: "M", A: "A", D: "D", R: "R", C: "C", U: "!", "?": "U" }; export function GitPanel({ repo, status, refresh, showDiff, openFile, }: { repo: string; status?: GitStatus; refresh: () => Promise; showDiff: (title: string, diff: string) => void; openFile: (path: string) => void; }) { const t = useT(); const [message, setMessage] = useState(""); const [amend, setAmend] = useState(false); const [busy, setBusy] = useState(); const [output, setOutput] = useState<{ ok: boolean; text: string }>(); const [branches, setBranches] = useState<{ name: string; current: boolean }[]>([]); const [log, setLog] = useState([]); const [view, setView] = useState<"changes" | "history">("changes"); const call = async (action: string, extra: Record = {}): Promise => post("/git", { path: repo, action, ...extra }); const run = async (label: string, fn: () => Promise, showOutput = false) => { setBusy(label); setOutput(undefined); try { const r: any = await fn(); if (showOutput) setOutput({ ok: true, text: (r?.output ?? "").trim() || t("git.done") }); await refresh(); void loadMeta(); } catch (e: any) { setOutput({ ok: false, text: e.message }); } finally { setBusy(undefined); } }; const loadMeta = async () => { try { const [b, l] = await Promise.all([call<{ branches: any[] }>("branches"), call<{ commits: Commit[] }>("log", { limit: 60 })]); setBranches(b.branches.filter((x: any) => !String(x.name).startsWith("origin/HEAD"))); setLog(l.commits); } catch { /* empty repository */ } }; // eslint-disable-next-line react-hooks/exhaustive-deps useEffect(() => void loadMeta(), [repo]); if (!status) return
; const abs = (f: GitFile) => `${status.repo}/${f.path}`; const staged = status.files.filter((f) => f.index !== " " && f.index !== "?"); const unstaged = status.files.filter((f) => f.worktree !== " " || f.index === "?"); const diff = async (f: GitFile, isStaged: boolean) => { try { const r = await call<{ diff: string }>("diff", { files: [abs(f)], staged: isStaged }); showDiff(`${f.path}${isStaged ? ` (${t("git.staged")})` : ""}`, r.diff); } catch (e: any) { toast(e.message); } }; const fileRow = (f: GitFile, isStaged: boolean) => { const code = isStaged ? f.index : f.index === "?" ? "?" : f.worktree; return (
{LABEL[code] ?? code} {isStaged ? ( ) : ( <> {f.index !== "?" && ( )} )}
); }; return (
{status.upstream && ( {status.ahead} {status.behind} )} {(["fetch", "pull", "push"] as const).map((a) => ( ))}
{output &&
{output.text}
}
{view === "changes" ? ( <>
{t("git.staged")} ({staged.length}) {staged.length > 0 && ( )}
{staged.map((f) => fileRow(f, true))}
{t("git.unstaged")} ({unstaged.length}) {unstaged.length > 0 && ( )}
{unstaged.map((f) => fileRow(f, false))} {status.files.length === 0 &&
{t("git.clean")}
}