2b5d7268ed
Server: git-rewrite.js (rebase + continue/skip/abort, rebase-interactive with no editor, reset soft/mixed/keep/hard, undo-rewrite, force-push with lease, remote-branch-delete, remote add/rename/remove) and git-conflicts.js (conflict stages read + conflict-save, reflog), dispatched from runGitAction; operationInProgress now reports a rebase and only ALLOWED_WHILE_REBASING writes run during one. New reads: /git/conflict, /git/reflog, /git/remotes, /git/rewrite-plan. Client: History tools menu, MergeEditor, ReflogView, RebaseDialog, InteractiveRebase, ResetDialog, ForcePushDialog, RemotesDialog, rebase banner Skip. CLAUDE.md git-writes rule rewritten for the new actions.
158 lines
5.2 KiB
TypeScript
158 lines
5.2 KiB
TypeScript
/**
|
|
* @file ReflogView.tsx
|
|
* @description The read-only reflog of HEAD or of one local branch, paged
|
|
* (GET /git/reflog): each entry's selector, what moved it (commit, rebase,
|
|
* reset, checkout…), git's message, when, and the commit it points at — a
|
|
* click opens that commit in the Changes panel. "Reset here" hands the entry
|
|
* to the reset dialog, which is how a lost commit is brought back.
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { api, type GitTarget } from "../../../lib/api";
|
|
import { errText, gitRewrite, type ReflogEntry } from "../../../lib/gitRewrite";
|
|
import { BTN, ErrorLine, GitModal } from "./GitModal";
|
|
|
|
const PAGE = 50;
|
|
|
|
function when(time: number | null, lang: string): string {
|
|
if (!time) return "";
|
|
try {
|
|
return new Date(time * 1000).toLocaleString(lang);
|
|
} catch {
|
|
return new Date(time * 1000).toISOString();
|
|
}
|
|
}
|
|
|
|
export function ReflogView({
|
|
target,
|
|
onClose,
|
|
onViewCommit,
|
|
onReset,
|
|
}: {
|
|
target: GitTarget;
|
|
onClose: () => void;
|
|
/** Open one commit (the panel's commit view). */
|
|
onViewCommit?: (sha: string) => void;
|
|
/** Open the reset dialog at this commit. */
|
|
onReset?: (entry: ReflogEntry) => void;
|
|
}) {
|
|
const { t, i18n } = useTranslation(["gitTools"]);
|
|
const [ref, setRef] = useState("HEAD");
|
|
const [branches, setBranches] = useState<string[]>([]);
|
|
const [entries, setEntries] = useState<ReflogEntry[] | null>(null);
|
|
const [more, setMore] = useState(false);
|
|
const [busy, setBusy] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const key = JSON.stringify(target);
|
|
|
|
useEffect(() => {
|
|
api.lanes
|
|
.gitBranches(target)
|
|
.then((b) => setBranches(b.branches ?? []))
|
|
.catch(() => setBranches([]));
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [key]);
|
|
|
|
const page = async (skip: number) => {
|
|
setBusy(true);
|
|
setError(null);
|
|
try {
|
|
const r = await gitRewrite.reflog(target, { ref, skip, limit: PAGE });
|
|
setEntries((prev) => (skip && prev ? [...prev, ...r.entries] : r.entries));
|
|
setMore(r.more);
|
|
} catch (e) {
|
|
setError(errText(e));
|
|
if (!skip) setEntries([]);
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
};
|
|
useEffect(() => {
|
|
setEntries(null);
|
|
void page(0);
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [key, ref]);
|
|
|
|
return (
|
|
<GitModal title={t("reflog.title")} onClose={onClose} testId="reflog-view">
|
|
<div className="mb-2 flex flex-wrap items-center gap-1.5">
|
|
<label className="text-[11px]" htmlFor="reflog-ref">
|
|
{t("reflog.of")}
|
|
</label>
|
|
<select
|
|
id="reflog-ref"
|
|
data-testid="reflog-ref"
|
|
value={ref}
|
|
onChange={(e) => setRef(e.target.value)}
|
|
className="input min-w-0 max-w-full flex-1 py-0.5 text-[11px]"
|
|
>
|
|
<option value="HEAD">HEAD</option>
|
|
{branches.map((b) => (
|
|
<option key={b} value={b}>
|
|
{b}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
<p className="mb-1.5 text-[11px] text-fg-muted">{t("reflog.hint")}</p>
|
|
<ErrorLine error={error} />
|
|
{entries == null ? (
|
|
<p className="text-fg-muted">{t("loading")}</p>
|
|
) : !entries.length ? (
|
|
<p className="text-fg-muted">{t("reflog.empty")}</p>
|
|
) : (
|
|
<ul data-testid="reflog-list" className="divide-y divide-border">
|
|
{entries.map((e) => (
|
|
<li key={`${e.index}-${e.sha}`} className="flex flex-wrap items-start gap-1.5 py-1.5">
|
|
<span className="shrink-0 font-mono text-[10px] text-fg-muted">{e.selector}</span>
|
|
<span className="shrink-0 rounded bg-surface-3 px-1 text-[10px] text-fg-secondary">
|
|
{e.action}
|
|
</span>
|
|
<span className="min-w-0 flex-1 break-words text-[11px] text-fg-primary">
|
|
{e.message || e.subject}
|
|
<span className="block text-[10px] text-fg-muted">
|
|
{when(e.time, i18n.language)}
|
|
</span>
|
|
</span>
|
|
<span className="flex shrink-0 items-center gap-1">
|
|
{onViewCommit ? (
|
|
<button
|
|
type="button"
|
|
className="font-mono text-[11px] text-accent hover:underline"
|
|
title={e.subject}
|
|
onClick={() => onViewCommit(e.sha)}
|
|
>
|
|
{e.short}
|
|
</button>
|
|
) : (
|
|
<span className="font-mono text-[11px]" title={e.subject}>
|
|
{e.short}
|
|
</span>
|
|
)}
|
|
{onReset && (
|
|
<button type="button" className={BTN} onClick={() => onReset(e)}>
|
|
{t("reflog.resetHere")}
|
|
</button>
|
|
)}
|
|
</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
{more && (
|
|
<button
|
|
type="button"
|
|
data-testid="reflog-more"
|
|
disabled={busy}
|
|
className={`${BTN} mt-2 w-full`}
|
|
onClick={() => void page(entries?.length ?? 0)}
|
|
>
|
|
{t("reflog.more")}
|
|
</button>
|
|
)}
|
|
</GitModal>
|
|
);
|
|
}
|