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.
120 lines
3.8 KiB
TypeScript
120 lines
3.8 KiB
TypeScript
/**
|
|
* @file ForcePushDialog.tsx
|
|
* @description "Force push (with lease)" for the CURRENT branch only: shows
|
|
* where it goes (remote and branch), the remote commit it will replace (the
|
|
* lease — the server refuses with ESTALE if the remote moved since the last
|
|
* fetch), how far ahead/behind HEAD is, and refuses a protected branch unless
|
|
* the server allows it. Push is a two-click (`useArmed`).
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import type { GitTarget } from "../../../lib/api";
|
|
import { errCode, errText, gitRewrite, type PushPlan } from "../../../lib/gitRewrite";
|
|
import { toast } from "../../../lib/toast";
|
|
import { useArmed } from "../../../hooks/useArmed";
|
|
import { BTN, BTN_DANGER, ErrorLine, GitModal, Warn } from "./GitModal";
|
|
|
|
export function ForcePushDialog({
|
|
target,
|
|
onClose,
|
|
onChanged,
|
|
}: {
|
|
target: GitTarget;
|
|
onClose: () => void;
|
|
onChanged: () => void;
|
|
}) {
|
|
const { t } = useTranslation(["gitTools"]);
|
|
const [plan, setPlan] = useState<PushPlan | null>(null);
|
|
const [busy, setBusy] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const { isArmed, arm, disarm } = useArmed();
|
|
const key = JSON.stringify(target);
|
|
|
|
useEffect(() => {
|
|
let live = true;
|
|
gitRewrite
|
|
.planPush(target)
|
|
.then((p) => live && setPlan(p))
|
|
.catch((e) => live && setError(errText(e)));
|
|
return () => {
|
|
live = false;
|
|
};
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [key]);
|
|
|
|
const run = async () => {
|
|
if (!plan) return;
|
|
if (!isArmed("push")) return arm("push");
|
|
disarm();
|
|
setBusy(true);
|
|
setError(null);
|
|
try {
|
|
await gitRewrite.run(target, "force-push", { expect: plan.remoteSha ?? "", confirm: true });
|
|
toast.success(t("forcePush.done", { remote: plan.remote, branch: plan.remoteBranch }));
|
|
onChanged();
|
|
onClose();
|
|
} catch (e) {
|
|
setError(errCode(e) === "ESTALE" ? t("forcePush.stale") : errText(e));
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<GitModal
|
|
title={t("forcePush.title")}
|
|
onClose={onClose}
|
|
testId="force-push-dialog"
|
|
footer={
|
|
<>
|
|
<button type="button" className={BTN} onClick={onClose}>
|
|
{t("cancel")}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
data-testid="force-push-run"
|
|
className={BTN_DANGER}
|
|
disabled={busy || !plan || !plan.allowed}
|
|
onClick={() => void run()}
|
|
>
|
|
{isArmed("push") ? t("forcePush.runConfirm") : t("forcePush.run")}
|
|
</button>
|
|
</>
|
|
}
|
|
>
|
|
{!plan ? (
|
|
error ? (
|
|
<ErrorLine error={error} />
|
|
) : (
|
|
<p className="text-fg-muted">{t("loading")}</p>
|
|
)
|
|
) : (
|
|
<div className="space-y-1.5">
|
|
<p data-testid="force-push-where" className="break-words">
|
|
{t("forcePush.where", {
|
|
branch: plan.branch,
|
|
remote: plan.remote,
|
|
remoteBranch: plan.remoteBranch,
|
|
})}
|
|
</p>
|
|
<p className="break-words">
|
|
{plan.remoteSha
|
|
? t("forcePush.replaces", {
|
|
sha: plan.remoteSha.slice(0, 7),
|
|
ahead: plan.ahead,
|
|
behind: plan.behind,
|
|
})
|
|
: t("forcePush.newBranch")}
|
|
</p>
|
|
{plan.behind > 0 && <Warn>{t("forcePush.dropsRemote", { count: plan.behind })}</Warn>}
|
|
{!plan.allowed && <Warn testId="force-push-protected">{t("forcePush.protected")}</Warn>}
|
|
<p className="text-[11px] text-fg-muted">{t("forcePush.lease")}</p>
|
|
<ErrorLine error={error} />
|
|
</div>
|
|
)}
|
|
</GitModal>
|
|
);
|
|
}
|