Files
AgentHub/client/src/components/lanes/git/CherryPickDialog.tsx
T

153 lines
5.1 KiB
TypeScript

/**
* @file The confirmation a cherry-pick or revert opens with, showing exactly
* what will happen before anything does (the reference tools' preview): the
* commits in the order git will apply them (numbered — read from the server,
* which orders them the same way the action will), the branch they land on,
* whether the working tree is clean, and what happens on a conflict. Confirm
* is disabled, with the reason, when the server would refuse (a dirty tree, a
* subfolder workspace, another operation open, a merge commit).
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { CheckCircle2, AlertTriangle } from "lucide-react";
import { api, type GitTarget } from "../../../lib/api";
import type { PickPlan, PickResult } from "../../../lib/gitHistory";
import { GitModal, ModalButton } from "./GitModal";
export function CherryPickDialog({
target,
op,
shas,
onClose,
onDone,
}: {
target: GitTarget;
op: "cherry-pick" | "revert";
shas: string[];
onClose: () => void;
onDone: (r: PickResult) => void;
}) {
const { t } = useTranslation(["gitHistory"]);
const [plan, setPlan] = useState<PickPlan | null>(null);
const [error, setError] = useState<string | null>(null);
const [running, setRunning] = useState(false);
useEffect(() => {
let live = true;
void api.lanes.gitHistory
.pickPlan(target, op, shas)
.then((r) => live && (r.available ? setPlan(r) : setError(t("planUnavailable"))))
.catch((err) => live && setError(err instanceof Error ? err.message : String(err)));
return () => {
live = false;
};
// eslint-disable-next-line react-hooks/exhaustive-deps -- one plan per dialog
}, []);
const branch = plan?.branch ?? "HEAD";
const blocker = !plan
? null
: plan.busy
? t("blockBusy")
: !plan.repoRoot
? t("blockSubdir")
: !plan.clean
? t("blockDirty")
: null;
const n = plan?.commits.length ?? shas.length;
const title =
op === "revert" ? t("revertTitle", { count: n }) : t("pickTitle", { count: n, branch });
const run = async () => {
setRunning(true);
setError(null);
try {
onDone(
await api.lanes.gitHistory.pick(
target,
op,
plan!.commits.map((c) => c.sha)
)
);
} catch (err) {
setError(err instanceof Error ? err.message : String(err));
setRunning(false);
}
};
return (
<GitModal
title={title}
onClose={onClose}
testId="pick-dialog"
footer={
<>
<ModalButton onClick={onClose}>{t("cancel")}</ModalButton>
<ModalButton
primary
testId="pick-confirm"
disabled={!plan || !!blocker || running}
title={blocker ?? undefined}
onClick={() => void run()}
>
{running
? t("running")
: op === "revert"
? t("revertN", { count: n })
: t("pickN", { count: n, branch })}
</ModalButton>
</>
}
>
<div className="space-y-2 text-[12px] text-fg-secondary">
{!plan && !error && <p className="text-fg-muted">{t("planning")}</p>}
{plan && (
<>
<p className="[overflow-wrap:anywhere]">
{op === "revert"
? t("revertIntro", { count: n, branch })
: t("pickIntro", { count: n, branch })}
</p>
<ol
data-testid="pick-plan"
className="space-y-0.5 rounded border border-border-light bg-surface-0 p-1.5"
>
{plan.commits.map((c, i) => (
<li key={c.sha} className="flex min-w-0 items-baseline gap-1.5">
<span className="w-5 shrink-0 text-right text-fg-muted">{i + 1}.</span>
<span className="shrink-0 font-mono text-[11px] text-fg-muted">{c.short}</span>
<span className="min-w-0 flex-1 truncate text-fg-primary" title={c.subject}>
{c.subject}
</span>
</li>
))}
</ol>
<p className="flex items-center gap-1.5">
{plan.clean && plan.repoRoot && !plan.busy ? (
<CheckCircle2 className="h-3.5 w-3.5 shrink-0 text-status-success" aria-hidden />
) : (
<AlertTriangle className="h-3.5 w-3.5 shrink-0 text-status-warning" aria-hidden />
)}
<span data-testid="pick-tree-state" className="[overflow-wrap:anywhere]">
{blocker ?? t("treeClean")}
</span>
</p>
<p className="text-fg-muted">{t(op === "revert" ? "revertResult" : "pickResult")}</p>
<p className="text-fg-muted">{t("onConflict", { op: t(`op.${op}`) })}</p>
</>
)}
{error && (
<p
role="alert"
className="whitespace-pre-wrap text-status-danger [overflow-wrap:anywhere]"
>
{error}
</p>
)}
</div>
</GitModal>
);
}